]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #5043: socks: socks inspector
authorRaza Shafiq (rshafiq) <rshafiq@cisco.com>
Thu, 29 Jan 2026 16:20:57 +0000 (16:20 +0000)
committerSteven Baigal (sbaigal) <sbaigal@cisco.com>
Thu, 29 Jan 2026 16:20:57 +0000 (16:20 +0000)
Merge in SNORT/snort3 from ~RSHAFIQ/snort3:socks to master

Squashed commit of the following:

commit 9ff4662694ab5ec8dd992b777c6efd9f2020809d
Author: rshafiq <rshafiq@cisco.com>
Date:   Tue Aug 19 14:00:03 2025 -0400

    socks: socks inspector

41 files changed:
doc/user/socks.txt [new file with mode: 0644]
lua/snort.lua
lua/snort_defaults.lua
src/detection/fp_utils.cc
src/flow/session.h
src/service_inspectors/CMakeLists.txt
src/service_inspectors/service_inspectors.cc
src/service_inspectors/socks/CMakeLists.txt [new file with mode: 0644]
src/service_inspectors/socks/dev_notes.txt [new file with mode: 0644]
src/service_inspectors/socks/socks.cc [new file with mode: 0644]
src/service_inspectors/socks/socks.h [new file with mode: 0644]
src/service_inspectors/socks/socks_event.h [new file with mode: 0644]
src/service_inspectors/socks/socks_flow_data.cc [new file with mode: 0644]
src/service_inspectors/socks/socks_flow_data.h [new file with mode: 0644]
src/service_inspectors/socks/socks_ips.cc [new file with mode: 0644]
src/service_inspectors/socks/socks_ips.h [new file with mode: 0644]
src/service_inspectors/socks/socks_module.cc [new file with mode: 0644]
src/service_inspectors/socks/socks_module.h [new file with mode: 0644]
src/service_inspectors/socks/socks_splitter.cc [new file with mode: 0644]
src/service_inspectors/socks/socks_splitter.h [new file with mode: 0644]
src/service_inspectors/socks/test/CMakeLists.txt [new file with mode: 0644]
src/service_inspectors/socks/test/socks_flow_data_test.cc [new file with mode: 0644]
src/service_inspectors/socks/test/socks_handoff_test.cc [new file with mode: 0644]
src/service_inspectors/socks/test/socks_ips_test.cc [new file with mode: 0644]
src/service_inspectors/socks/test/socks_module_test.cc [new file with mode: 0644]
src/service_inspectors/socks/test/socks_negative_test.cc [new file with mode: 0644]
src/service_inspectors/socks/test/socks_splitter_negative_test.cc [new file with mode: 0644]
src/service_inspectors/socks/test/socks_splitter_test.cc [new file with mode: 0644]
src/service_inspectors/wizard/CMakeLists.txt
src/service_inspectors/wizard/curse_book.cc
src/service_inspectors/wizard/curse_book.h
src/service_inspectors/wizard/socks_curse.cc [new file with mode: 0644]
src/service_inspectors/wizard/socks_curse.h [new file with mode: 0644]
src/service_inspectors/wizard/wiz_module.cc
src/stream/stream.cc
src/stream/stream.h
src/stream/tcp/tcp_reassembler.h
src/stream/tcp/tcp_session.cc
src/stream/tcp/tcp_session.h
src/stream/tcp/tcp_stream_tracker.cc
src/stream/tcp/tcp_stream_tracker.h

diff --git a/doc/user/socks.txt b/doc/user/socks.txt
new file mode 100644 (file)
index 0000000..1421750
--- /dev/null
@@ -0,0 +1,443 @@
+socks inspector is a service inspector for the SOCKS protocol, a circuit-level
+proxy protocol that operates at the session layer.
+
+==== Overview
+
+SOCKS (Socket Secure) is a protocol that relays traffic through a proxy server
+without interpreting it, making it protocol-agnostic. Unlike application-level
+proxies, SOCKS operates at the session layer and can tunnel any TCP or UDP
+traffic.
+
+SOCKS does not define a specific TCP port for its use, but port 1080 is
+commonly used by convention.
+
+The `socks` service inspector decodes SOCKS4/4a and SOCKS5 handshake messages,
+extracts tunnel metadata (target address, port, command type), and provides
+rule options to access protocol fields. After successful handshake, the
+inspector hands off tunneled traffic to the wizard for protocol detection,
+allowing Snort to inspect the actual tunneled protocols (HTTP, HTTPS, SMTP,
+DNS, etc.) rather than just the SOCKS wrapper.
+
+==== Detection Flow
+
+SOCKS detection in Snort3 uses a two-stage process for optimal performance:
+
+1. **Initial Detection (Wizard Curse)**: The wizard's SOCKS curse performs fast
+   pattern matching on the first application-layer bytes to identify SOCKS
+   traffic. It detects SOCKS4/4a client greetings (version + command + port +
+   IP + userid) and SOCKS5 client greetings (version + nmethods + methods).
+   The curse uses strict validation to minimize false positives, particularly
+   on protocols like SQL Server that may have similar byte patterns.
+
+2. **Full Inspection (Service Inspector)**: Once the wizard classifies a flow
+   as SOCKS, the full service inspector is bound to the flow. The inspector
+   processes the complete handshake with state machine validation, extracts
+   tunnel metadata, and provides IPS rule options for detection.
+
+This approach provides fast rejection of non-SOCKS traffic while maintaining
+comprehensive protocol analysis for confirmed SOCKS flows.
+
+==== UDP ASSOCIATE Flow Binding
+
+For SOCKS5 UDP ASSOCIATE, the inspector creates a dynamic UDP flow expectation
+when the server sends a successful reply with the bind address and port:
+
+When the TCP control channel completes the UDP ASSOCIATE handshake, the
+inspector automatically creates an expected flow for UDP traffic between the
+client and the proxy's UDP relay endpoint (BND.ADDR:BND.PORT from the server
+response). This expected flow is bidirectional, allowing both client-to-proxy
+and proxy-to-client UDP packets to be automatically bound to the SOCKS
+inspector without requiring additional wizard pattern matching.
+
+The expected UDP flow is pre-configured with SOCKS5 UDP ASSOCIATE state, so
+incoming UDP packets are immediately recognized as SOCKS-UDP traffic. The
+inspector then strips the SOCKS5-UDP header and hands off the inner payload to
+the wizard for protocol detection (DNS, SIP, etc.).
+
+==== Configuration
+
+SOCKS messages can be sent across multiple TCP packets, and the `socks` service
+inspector normalizes the traffic such that only complete SOCKS messages are
+presented. The inspector supports both TCP tunneling (CONNECT), reverse
+connections (BIND), and UDP relay (UDP ASSOCIATE).
+
+The SOCKS inspector supports the following configuration option:
+
+* `block_udp_fragmentation` (boolean, default: true) - Block entire flow when
+  SOCKS5 UDP fragmentation is detected (frag > 0). When disabled, only the
+  fragmented packets are dropped and an alert is generated. Strict blocking is
+  recommended for high-security environments as UDP fragmentation is rarely
+  used in legitimate traffic (RFC 1928 ยง7) and can indicate evasion attempts or
+  DoS attacks.
+
+
+==== Quick Guide
+
+A typical SOCKS configuration looks like the following:
+
+   wizard = { curses = {'socks'}, }
+   socks = { }
+   
+   binder =
+   {
+       { when = { service = 'socks' }, use = { type = 'socks' } },
+       { use = { type = 'wizard' } }
+   }
+
+In this example, the `socks` inspector is defined based on patterns known to
+be consistent with SOCKS messages.
+
+UDP fragmentation blocking is strict by default. To allow fragmented SOCKS5
+UDP packets (drop only the fragments and keep the flow alive), disable the
+flag explicitly:
+
+   socks =
+   {
+       block_udp_fragmentation = false
+   }
+
+
+==== Rule Options
+
+New rule options are supported by enabling the `socks` inspector:
+
+* socks_version
+* socks_state
+* socks_command
+* socks_address_type
+* socks_remote_address
+* socks_remote_port
+
+===== socks_version
+
+`socks_version` takes the supplied version number as an integer and compares
+it with the version field in the SOCKS message being analyzed. Valid values
+are 4 (SOCKS4/4a) and 5 (SOCKS5).
+
+This option takes one argument.
+
+In the following example, the rule is using the `socks_version` rule option
+with an integer argument to match SOCKS5 traffic. This is combined with a
+content match for the version byte as a fast pattern.
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS5 connection detected"; \
+      flow: to_server, established; \
+      content:"|05|", depth 1; \
+      socks_version:5; \
+      sid:1000000; \
+    )
+
+===== socks_state
+
+`socks_state` matches the SOCKS inspector state for the flow. It accepts
+either a numeric value or a name:
+
+* 1 (auth) - SOCKS5 authentication negotiation or username/password auth
+* 2 (request_response) - request/response phase for SOCKS4/4a and SOCKS5
+* 3 (established) - successful handshake, tunnel established
+* 4 (error) - handshake failed (protocol violation, auth failure, connect failure)
+
+This option takes one argument.
+
+In the following example, the rule sets a flowbit once a SOCKS tunnel is
+established:
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS tunnel established"; \
+      flow: to_server, established; \
+      socks_state:established; \
+      flowbits:set,socks.tunnel; \
+      sid:1000003; \
+    )
+
+
+===== socks_command
+
+`socks_command` takes the supplied command type as an integer and compares it
+with the command field in the SOCKS message being analyzed. Valid values are:
+1 (CONNECT - TCP tunneling), 2 (BIND - reverse connections), and 3 (UDP
+ASSOCIATE - UDP relay, SOCKS5 only).
+
+This option takes one argument.
+
+In the following example, the rule is using the `socks_command` rule option
+to detect CONNECT commands. This is combined with a content match for the
+SOCKS5 version byte.
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS5 CONNECT command detected"; \
+      flow: to_server, established; \
+      content:"|05|"; \
+      socks_command:1; \
+      sid:1000001; \
+    )
+
+
+===== socks_address_type
+
+`socks_address_type` takes the supplied address type as an integer and
+compares it with the address type field in the SOCKS5 message being analyzed.
+Valid values are: 1 (IPv4), 3 (Domain name), and 4 (IPv6). This option is
+only applicable to SOCKS5 traffic.
+
+This option takes one argument.
+
+In the following example, the rule is using the `socks_address_type` rule
+option to detect domain name requests. This is combined with a content match
+for the SOCKS5 version byte.
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS5 domain name request detected"; \
+      flow: to_server, established; \
+      content:"|05|"; \
+      socks_address_type:3; \
+      sid:1000002; \
+    )
+
+
+===== socks_remote_address
+
+`socks_remote_address` sets the cursor to the target destination address in
+the SOCKS message. The address can be a domain name or an IP address string,
+depending on the address type. This buffer option is typically used with the
+`content` keyword to match specific destinations.
+
+This option takes no arguments.
+
+In the following example, the rule is using the `socks_remote_address` buffer
+option to set the cursor, then uses `content` to match a malicious domain.
+Note that `socks_version:5` is used to trigger rule evaluation on SOCKS traffic.
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS5 tunnel to malicious domain"; \
+      flow: to_server, established; \
+      socks_version:5; \
+      socks_remote_address; \
+      content:"evil.com"; \
+      sid:1000003; \
+    )
+
+The following example detects tunnels to a specific IP address:
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS5 tunnel to C2 server"; \
+      flow: to_server, established; \
+      socks_version:5; \
+      socks_remote_address; \
+      content:"203.0.113.50"; \
+      sid:1000004; \
+    )
+
+
+===== socks_remote_port
+
+`socks_remote_port` takes the supplied port number as an integer and compares
+it with the target destination port in the SOCKS message being analyzed. This
+allows detection of tunnels to specific services.
+
+This option takes one argument.
+
+In the following example, the rule is using the `socks_remote_port` rule
+option to detect tunnels to HTTPS (port 443). This is combined with a content
+match for the SOCKS5 version byte.
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS5 tunnel to HTTPS"; \
+      flow: to_server, established; \
+      content:"|05|"; \
+      socks_remote_port:443; \
+      sid:1000005; \
+    )
+
+Multiple rule options can be combined for more specific detection:
+
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS5 CONNECT to suspicious domain on port 443"; \
+      flow: to_server, established; \
+      socks_version:5; \
+      socks_command:1; \
+      socks_address_type:3; \
+      socks_remote_address; \
+      content:"suspicious.com"; \
+      socks_remote_port:443; \
+      sid:1000006; \
+    )
+
+Note: When using `socks_remote_address` with `content:`, do not include a
+packet content fast pattern (like `content:"|05|"`) before the buffer option,
+as this will prevent the buffer content match from working correctly. SOCKS IPS
+options like `socks_version:5` provide the necessary trigger for rule evaluation.
+
+
+==== Advanced Use Case: Blocking Tunneled File Traffic
+
+A common security requirement is to block file transfers over SOCKS tunnels
+(e.g., data exfiltration) while allowing direct file transfers. This requires
+distinguishing between "HTTP over SOCKS" and "HTTP direct".
+
+The recommended approach uses `flowbits` to tag SOCKS tunnels, then conditionally
+blocks file traffic based on that tag:
+
+    # Step 1: Tag SOCKS CONNECT tunnels with a flowbit
+    alert tcp any any -> any 1080 ( \
+      msg: "SOCKS tunnel established"; \
+      flow: to_server, established; \
+      socks_command:1; \
+      flowbits:set,socks.tunnel; \
+      flowbits:noalert; \
+      sid:1000100; \
+    )
+
+    # Step 2: Block file downloads only if tunneled through SOCKS
+    drop tcp any any -> any any ( \
+      msg: "Blocked file download over SOCKS tunnel"; \
+      flow: from_server, established; \
+      flowbits:isset,socks.tunnel; \
+      file_data; \
+      content:"application/octet-stream"; http_header; \
+      sid:1000101; \
+    )
+
+    # Step 3: Allow direct file downloads (no SOCKS tunnel)
+    # (No rule needed - traffic without socks.tunnel flowbit passes through)
+
+In this example:
+
+* Rule 1000100 sets the `socks.tunnel` flowbit when a SOCKS CONNECT command
+  is detected. The `flowbits:noalert` prevents this from generating an alert.
+
+* Rule 1000101 blocks file downloads (identified by Content-Type header) only
+  when the `socks.tunnel` flowbit is set. This rule triggers on the tunneled
+  HTTP response traffic after wizard hands off to HTTP inspector.
+
+* Direct HTTP file downloads (without SOCKS) do not have the flowbit set and
+  are not blocked.
+
+This approach works because:
+
+1. SOCKS handshake completes first, setting the flowbit on the flow
+2. SOCKS inspector hands off tunneled traffic to wizard
+3. Wizard detects HTTP and hands off to HTTP inspector
+4. HTTP inspector processes the tunneled HTTP traffic
+5. File detection rules check the flowbit to determine if traffic is tunneled
+
+You can extend this pattern to block other tunneled protocols:
+
+    # Block SMTP over SOCKS (email exfiltration)
+    drop tcp any any -> any any ( \
+      msg: "Blocked SMTP over SOCKS tunnel"; \
+      flow: established; \
+      flowbits:isset,socks.tunnel; \
+      content:"MAIL FROM"; \
+      sid:1000102; \
+    )
+
+    # Block FTP over SOCKS
+    drop tcp any any -> any any ( \
+      msg: "Blocked FTP over SOCKS tunnel"; \
+      flow: established; \
+      flowbits:isset,socks.tunnel; \
+      content:"STOR "; depth 5; \
+      sid:1000103; \
+    )
+
+
+==== Built-in Rules
+
+The SOCKS inspector includes 4 built-in rules (GID 155) that detect protocol
+anomalies and potential security threats:
+
+* 155:1 - SOCKS unknown command
+  Detects malformed/malicious requests with invalid command bytes. Indicates
+  exploit attempts or protocol fuzzing.
+
+* 155:2 - SOCKS protocol violation
+  Detects protocol violations such as non-zero reserved fields. Critical
+  security indicator for protocol confusion attacks, exploits, or evasion.
+
+* 155:3 - SOCKS5 unknown address type
+  Detects malformed requests with invalid address types. Indicates exploit
+  attempts or fuzzing.
+
+* 155:4 - SOCKS5 UDP fragmentation detected
+  Detects SOCKS5 UDP packets with fragmentation (frag > 0). UDP fragmentation
+  is rarely used in legitimate traffic (RFC 1928 ยง7) and can indicate evasion
+  techniques or DoS attacks. When `block_udp_fragmentation` remains enabled
+  (default), this also triggers flow blocking.
+
+
+==== Events and Logging
+
+The SOCKS inspector publishes events via DataBus for external consumption and
+logs target destination IP to unified2 as XFF (X-Forwarded-For) data for
+correlation with SIEM/FMC. This allows security analysts to see the true
+destination hidden inside the SOCKS tunnel, not just the proxy address.
+
+Example unified2 alert:
+
+    Alert: SOCKS5 tunnel to C2 server
+      SRC: 192.168.1.100 (client - from packet header)
+      DST: 10.0.0.5 (proxy - from packet header)
+      XFF: 203.0.113.50 (target - from SOCKS protocol)
+
+
+==== Protocol Support
+
+The SOCKS inspector supports:
+
+* SOCKS4/4a: CONNECT command (TCP tunneling), BIND command (reverse
+  connections), and domain name resolution via SOCKS4a extension.
+
+* SOCKS5: Authentication methods (No Auth, Username/Password), CONNECT
+  command (TCP tunneling), BIND command (reverse connections), UDP ASSOCIATE
+  command (UDP relay), and address types (IPv4, Domain, IPv6).
+
+After successful handshake, tunneled traffic is handed off to the wizard for
+protocol detection, enabling inspection of HTTP, HTTPS, SMTP, DNS, and other
+protocols tunneled through SOCKS.
+
+
+==== Known Limitations
+
+* Authentication: Only No Auth (0x00) and Username/Password (0x02) are fully
+  supported. GSSAPI (0x01) and private methods (0x80-0xFE) skip the auth
+  phase but still allow tunnel metadata extraction.
+
+* UDP Fragmentation: Standalone UDP packets (FRAG=0) are processed and sent
+  to wizard for protocol detection. Fragmented packets (FRAGโ‰ 0) are not
+  reassembled and cause flow inspection to be disabled to prevent false
+  positives.
+
+* Encrypted SOCKS: SOCKS over TLS/SSL is not supported (wizard would detect
+  SSL first, preventing SOCKS detection).
+
+* XFF Logging: The SOCKS inspector logs the target destination IP as XFF
+  (X-Forwarded-For) data to unified2 for correlation with SIEM/FMC. However,
+  XFF data has timing and availability constraints:
+  
+  - XFF is only available after the SOCKS CONNECT request is parsed (when
+    target IP becomes known), not during earlier handshake phases.
+  
+  - XFF is not available for domain-based targets (SOCKS4a or SOCKS5 address
+    type 3), only for IPv4/IPv6 targets.
+  
+  - XFF data is lost after protocol handoff to wizard/other inspectors. To
+    capture XFF in alerts, rules must trigger on the SOCKS CONNECT request
+    using `socks_command:1`, not on later tunneled traffic.
+  
+  - Example rule that captures XFF correctly:
+    
+      alert tcp any any -> any 1080 ( \
+        msg: "SOCKS tunnel with XFF logging"; \
+        socks_command:1; \
+        sid:1000200; \
+      )
+
+
+==== References
+
+* RFC 1928: SOCKS Protocol Version 5
+* RFC 1929: Username/Password Authentication for SOCKS V5
+* SOCKS4 Protocol: http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol
+* SOCKS4a Extension: http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4A.protocol
index bb93dbc5a151bb79798cc35ba2d2e63e3e9693f0..474c45e91905e1ef551461c0d2be2054c47d3d5f 100644 (file)
@@ -58,6 +58,7 @@ normalizer = { }
 pop = { }
 rpc_decode = { }
 sip = { }
+socks = { }
 ssh = { }
 ssl = { }
 telnet = { }
@@ -156,6 +157,7 @@ binder =
     { when = { service = 'ssh' },              use = { type = 'ssh' } },
     { when = { service = 'sip' },              use = { type = 'sip' } },
     { when = { service = 'smtp' },             use = { type = 'smtp' } },
+    { when = { service = 'socks' },            use = { type = 'socks' } },
     { when = { service = 'ssl' },              use = { type = 'ssl' } },
     { when = { service = 'sunrpc' },           use = { type = 'rpc_decode' } },
     { when = { service = 's7commplus' },       use = { type = 's7commplus' } },
index 009e39bee33015c5f024bd456762c7d9018b6f2b..3f87cf47ffc21a2a44902b1be395a5b27009009c 100644 (file)
@@ -415,7 +415,7 @@ default_wizard =
           to_server = telnet_commands, to_client = telnet_commands },
     },
 
-    curses = {'dce_udp', 'dce_tcp', 'dce_smb', 'mms', 'opcua', 's7commplus', 'sslv2'}
+    curses = {'dce_udp', 'dce_tcp', 'dce_smb', 'mms', 'opcua', 's7commplus', 'socks', 'sslv2'}
 }
 
 ---------------------------------------------------------------------------
index 2dcc02474cd111244c9db720945881687c3d3872..ae868d65f9447c4bc63bb56747c20bd5375f4da2 100644 (file)
@@ -184,6 +184,9 @@ static const char* guess_service(const char* opt)
     if ( !strncmp(opt, "sip_", 4) )
         return "sip";
 
+    if ( !strncmp(opt, "socks_", 6) )
+        return "socks";
+
     if ( !strncmp(opt, "ssl_", 4) )
         return "ssl";
 
index c2a694b6a23da4d6e1d7753cf99cc6b2c22057d5..76a9ab594e8c63cc5e82eb0d2387e0fcba9dd5ec 100644 (file)
@@ -66,6 +66,8 @@ public:
     virtual void flush_listener(snort::Packet*, bool /*final_flush */ = false) { }
 
     virtual void set_splitter(bool /*c2s*/, snort::StreamSplitter*) { assert(false); }
+    virtual void set_splitter_with_rescan(bool /*c2s*/, snort::StreamSplitter*, uint32_t /*seq*/) { assert(false); }
+    virtual uint32_t get_paf_position(bool /*c2s*/) const { return 0; }
     virtual snort::StreamSplitter* get_splitter(bool /*c2s*/) { return nullptr; }
 
     virtual void set_extra_data(snort::Packet*, uint32_t /*flag*/) { }
index 1816d4c2e2e8562bccfdee795c82368901161acd..a031c183ec98fe479a7d4be7a64fedab86b33a66 100644 (file)
@@ -23,6 +23,7 @@ add_subdirectory(ssh)
 add_subdirectory(ssl)
 add_subdirectory(tlv_pdu)
 add_subdirectory(wizard)
+add_subdirectory(socks)
 
 if (STATIC_INSPECTORS)
     set (STATIC_INSPECTOR_OBJS
@@ -54,6 +55,7 @@ set(STATIC_SERVICE_INSPECTOR_PLUGINS
     $<TARGET_OBJECTS:http2_inspect>
     $<TARGET_OBJECTS:sip>
     $<TARGET_OBJECTS:dns>
+    $<TARGET_OBJECTS:socks>
     ${STATIC_INSPECTOR_OBJS}
     CACHE INTERNAL "STATIC_SERVICE_INSPECTOR_PLUGINS"
 )
index 0f7ef84d088e8780aaa69d8e856897f2fad0395a..91e9a3371843ca443c3c180c03fb6507cfb6e08e 100644 (file)
@@ -32,6 +32,7 @@ extern const BaseApi* sin_file[];
 extern const BaseApi* sin_http[];
 extern const BaseApi* sin_http2[];
 extern const BaseApi* sin_sip[];
+extern const BaseApi* sin_socks[];
 
 #ifdef STATIC_INSPECTORS
 extern const BaseApi* sin_bo;
@@ -90,6 +91,7 @@ void load_service_inspectors()
     PluginManager::load_plugins(sin_http);
     PluginManager::load_plugins(sin_http2);
     PluginManager::load_plugins(sin_sip);
+    PluginManager::load_plugins(sin_socks);
 
 #ifdef STATIC_INSPECTORS
     PluginManager::load_plugins(sin_cip);
diff --git a/src/service_inspectors/socks/CMakeLists.txt b/src/service_inspectors/socks/CMakeLists.txt
new file mode 100644 (file)
index 0000000..a68c2c2
--- /dev/null
@@ -0,0 +1,34 @@
+set(SOCKS_INCLUDES
+    socks_module.h
+    socks.h
+    socks_flow_data.h
+    socks_splitter.h
+    socks_event.h
+    socks_ips.h
+)
+
+set(SOCKS_SOURCES
+    ${SOCKS_INCLUDES}
+    socks_module.cc
+    socks.cc
+    socks_flow_data.cc
+    socks_splitter.cc
+    socks_ips.cc
+)
+
+#if(STATIC_INSPECTORS)
+    add_library(socks OBJECT ${SOCKS_SOURCES})
+
+#else()
+    # SOCKS cannot be built as a dynamic module because it uses
+    # Analyzer::get_local_analyzer() which is only available in the main binary
+    #add_dynamic_module(socks inspectors ${SOCKS_SOURCES})
+
+#endif()
+
+target_include_directories(socks PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
+
+# Add test directory if testing is enabled
+if(ENABLE_UNIT_TESTS)
+    add_subdirectory(test)
+endif()
diff --git a/src/service_inspectors/socks/dev_notes.txt b/src/service_inspectors/socks/dev_notes.txt
new file mode 100644 (file)
index 0000000..3ba3c6f
--- /dev/null
@@ -0,0 +1,157 @@
+The SOCKS inspector processes stream reassembled packets and decodes SOCKS4/4a
+and SOCKS5 protocols. SOCKS is a circuit-level proxy protocol that operates at
+the session layer and relays traffic without interpreting it, making it
+protocol-agnostic.
+
+The inspector supports TCP tunneling (CONNECT), reverse connections (BIND), and
+UDP relay (UDP ASSOCIATE). It detects the SOCKS handshake, extracts tunnel
+metadata (target address, port, command type), and hands off the tunneled
+traffic to the wizard for protocol detection. This allows Snort to inspect the
+actual tunneled protocols (HTTP, HTTPS, SMTP, DNS, etc.) rather than just the
+SOCKS wrapper.
+
+SOCKS does not define a specific TCP port for its use, but port 1080 is
+commonly used by convention.
+
+==== Detection Flow
+
+SOCKS detection in Snort3 follows a two-stage process:
+
+1. Initial Protocol Detection (Wizard Curse):
+   - Wizard curse function (socks_curse in wizard/socks_curse.cc) performs fast
+     initial detection on first application-layer bytes
+   - Detects SOCKS4/4a client greeting (VER + CMD + PORT + IP + USERID)
+   - Detects SOCKS5 client greeting (VER + NMETHODS + METHODS)
+   - Uses stateless pattern matching with strict validation to reduce false
+     positives (e.g., SQL Server on port 1433)
+   - Returns true on successful detection, triggering flow classification as
+     "socks" service
+   - Curse is invoked at start of TCP stream (byte 0 of application data)
+
+2. Full Protocol Inspection (Service Inspector):
+   - After wizard classifies flow as "socks", full SOCKS inspector is bound
+   - Inspector processes complete handshake with state machine validation
+   - Extracts tunnel metadata (target address, port, command type)
+   - Provides IPS options for rule matching (socks_version, socks_command, etc.)
+   - Triggers handoff to wizard after tunnel establishment for tunneled protocol
+     detection
+
+This two-stage approach provides fast rejection of non-SOCKS traffic (curse)
+while maintaining comprehensive protocol analysis for confirmed SOCKS flows
+(inspector).
+
+==== Architecture
+
+The SOCKS inspector is divided into several major components:
+
+SocksSplitter (socks_splitter.cc/h) - Stream splitter that identifies SOCKS
+message boundaries. Handles both client-to-server and server-to-client traffic.
+Implements state machine for SOCKS4/4a/5 handshake phases. Returns FLUSH when
+complete message is available.
+
+SocksInspector (socks.cc/h) - Main inspector that processes flushed SOCKS
+messages. Parses handshake messages and extracts tunnel metadata. Manages flow
+data and state transitions. Triggers handoff to wizard after tunnel
+establishment. Publishes events via DataBus for logging/correlation.
+
+SocksFlowData (socks_flow_data.cc/h) - Persistent per-flow state storage.
+Tracks handshake progress, target address/port, protocol version. Implements
+state machine with 14 states (see SocksState enum).
+
+IPS Options (ips_socks_protocol.cc/h) - Protocol matchers: socks_version,
+socks_command, socks_address_type. Buffer options: socks_remote_address (target
+domain/IP), socks_remote_port. Enable rule-based detection on SOCKS tunnel
+metadata.
+
+==== State Machine
+
+The SOCKS inspector implements a comprehensive state machine to track handshake
+progress and ensure correct message ordering. State transitions are enforced to
+prevent protocol confusion attacks. Invalid state transitions trigger warnings
+and connection termination.
+
+SOCKS4/4a flow:
+INIT -> CLIENT_HELLO -> WAIT_SERVER_CONNECT_REPLY -> TUNNEL_ESTABLISHED
+
+SOCKS5 flow (No Auth):
+INIT -> CLIENT_HELLO -> WAIT_SERVER_AUTH_REPLY -> WAIT_CLIENT_CONNECT_REQUEST
+-> WAIT_SERVER_CONNECT_REPLY -> TUNNEL_ESTABLISHED
+
+SOCKS5 flow (Username/Password):
+INIT -> CLIENT_HELLO -> WAIT_SERVER_AUTH_REPLY -> WAIT_CLIENT_AUTH_REQUEST ->
+WAIT_SERVER_AUTH_RESPONSE -> WAIT_CLIENT_CONNECT_REQUEST ->
+WAIT_SERVER_CONNECT_REPLY -> TUNNEL_ESTABLISHED
+
+SOCKS5 UDP ASSOCIATE:
+... -> WAIT_SERVER_CONNECT_REPLY -> UDP_ASSOCIATE_ESTABLISHED
+
+==== Handoff Mechanism
+
+After successful SOCKS handshake, the inspector triggers immediate handoff to
+the wizard for protocol detection of tunneled traffic.
+
+TCP Tunneling (CONNECT): Handoff occurs after server sends success reply
+(0x00). Wizard examines first bytes of tunneled traffic. Common protocols:
+HTTP, HTTPS/TLS, SMTP, FTP, SSH.
+
+PAF Rescan for Piggybacked Data:
+Tunneled protocol data can be "piggybacked" on the same packet as the SOCKS
+CONNECT response, or queued in TCP reassembly segments. This happens in both
+normal and reverse SOCKS flows. The Protocol-Aware Flushing (PAF) system tracks
+which bytes have been "scanned" and won't re-scan them for a new splitter. To
+handle this, the inspector uses Stream::get_paf_position() to get where the
+SOCKS splitter left off, then Stream::set_splitter_with_rescan() to install
+the wizard's splitter with a reset PAF position. This allows the wizard to
+detect protocols in data that was already scanned by the SOCKS splitter.
+
+For reverse SOCKS (server initiates SOCKS), the wizard splitter directions are
+swapped so the wizard sees tunneled protocol data in the correct direction.
+
+Reverse Connection (BIND): Handoff occurs after second server reply (incoming
+connection). Wizard detects protocol of reverse connection.
+
+UDP Relay (UDP ASSOCIATE): Standalone UDP packets (FRAG=0) are handed off to
+wizard via pseudo-packet injection for protocol detection. Fragmented packets
+are dropped without handoff.
+
+==== UDP Processing
+
+SOCKS5 UDP ASSOCIATE is detected on the TCP control channel. When the server
+sends a successful reply (REP=0x00) with BND.ADDR and BND.PORT, the inspector
+creates a dynamic UDP expectation:
+
+Expected Flow Creation:
+- Source: SOCKS client IP (from TCP control flow)
+- Source Port: Any (0)
+- Destination: BND.ADDR (proxy's UDP relay endpoint from server response)
+- Destination Port: BND.PORT (from server response)
+- Protocol: UDP
+- Bidirectional: Yes (allows both client->proxy and proxy->client UDP packets)
+- Flow Data: Pre-configured SocksFlowData with UDP_ASSOCIATE state
+
+This expectation automatically binds matching UDP packets to the SOCKS inspector
+without requiring wizard pattern matching. The pre-configured flow data ensures
+UDP packets are immediately recognized as SOCKS5-UDP traffic.
+
+For UDP packets received on the expected flow:
+
+Standalone packets (FRAG=0x00): Validated, SOCKS5-UDP header stripped, and
+inner payload sent to wizard via pseudo-packet for protocol detection (DNS,
+SIP, etc.).
+
+Fragmented packets (FRAGโ‰ 0x00): Detected, alerted (GID 155:4), and dropped.
+Flow inspection is disabled in both directions to avoid false positives. If
+block_udp_fragmentation is enabled, the entire flow is blocked via
+block_session(). UDP fragmentation reassembly is not supported.
+
+==== Unified2 Extra Data
+
+The inspector logs target destination IP to unified2 as XFF (X-Forwarded-For)
+data for correlation with SIEM/FMC. This allows security analysts to see the
+true destination hidden inside the SOCKS tunnel, not just the proxy address.
+
+Example:
+  SRC: 192.168.1.100 (client - from packet header)
+  DST: 10.0.0.5 (proxy - from packet header)
+  XFF: 203.0.113.50 (target - from SOCKS protocol)
+
diff --git a/src/service_inspectors/socks/socks.cc b/src/service_inspectors/socks/socks.cc
new file mode 100644 (file)
index 0000000..9ea2226
--- /dev/null
@@ -0,0 +1,1764 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <algorithm>
+#include <cstring>
+
+#include "detection/detection_engine.h"
+#include "flow/flow.h"
+#include "flow/session.h"
+#include "framework/inspector.h"
+#include "main/snort_config.h"
+#include "log/messages.h"
+#include "packet_io/active.h"
+#include "profiler/profiler.h"
+#include "protocols/packet_manager.h"
+#include "log/unified2.h"
+#include "main/analyzer.h"
+#include "packet_io/packet_tracer.h"
+#include "protocols/eth.h"
+#include "protocols/ip.h"
+#include "protocols/udp.h"
+#include "protocols/packet.h"
+#include "pub_sub/intrinsic_event_ids.h"
+#include "sfip/sf_ip.h"
+#include "socks.h"
+#include "socks_event.h"
+#include "socks_flow_data.h"
+#include "socks_module.h"
+#include "socks_splitter.h"
+#include "stream/stream.h"
+#include "utils/util.h"
+
+using namespace snort;
+
+THREAD_LOCAL ProfileStats socksPerfStats;
+THREAD_LOCAL SocksStats socks_stats;
+
+static unsigned socks_pub_id = 0;
+static SnortProtocolId socks_snort_protocol_id = UNKNOWN_PROTOCOL_ID;
+
+SocksInspector::SocksInspector(const SocksModule* mod) : 
+    config(mod),
+    xtra_target_ip_id(Stream::reg_xtra_data_cb(get_xtra_target_ip))
+{ }
+
+bool SocksInspector::configure(SnortConfig* sc)
+{
+    if ( !socks_pub_id )
+        socks_pub_id = DataBus::get_id(socks_pub_key);
+
+    // Register protocol ID for dynamic UDP expectations (UDP ASSOCIATE)
+    if ( socks_snort_protocol_id == UNKNOWN_PROTOCOL_ID )
+        socks_snort_protocol_id = sc->proto_ref->add("socks");
+
+    return true;
+}
+
+void SocksInspector::show(const SnortConfig*) const
+{
+    if ( !config or !config->get_config() )
+        return;
+
+    ConfigLogger::log_flag("block_udp_fragmentation", config->get_config()->block_udp_fragmentation);
+}
+
+void SocksInspector::eval(Packet* p)
+{
+    // cppcheck-suppress unreadVariable
+    Profile profile(socksPerfStats);
+
+    assert((p->is_udp() and p->dsize and p->data) or p->has_tcp_data() or p->has_paf_payload());
+    assert(p->flow);
+
+    if ( !config or !config->get_config() )
+        return;
+
+    Flow* flow = p->flow;
+    SocksFlowData* flow_data = get_flow_data(flow);
+    if ( !flow_data )
+    {
+        create_flow_data(flow);
+        flow_data = get_flow_data(flow);
+        assert(flow_data);  // Should never be null after create_flow_data
+    }
+
+    SetExtraData(p, xtra_target_ip_id);
+
+    if ( flow_data->is_handoff_completed() )
+        return;
+
+    if ( !flow_data->initiator_detected() )
+        detect_protocol_initiator(p, flow_data);
+
+    if ( p->is_udp() )
+    {
+        // If SOCKS inspector is called for UDP, user explicitly bound this port
+        // to SOCKS via binder config. Trust the binding and process as UDP ASSOCIATE.
+        // Note: We don't auto-detect UDP via wizard patterns due to false positive risk.
+        if ( flow_data->get_state() == SOCKS_STATE_INIT )
+        {
+            flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+            flow_data->set_command(SOCKS_CMD_UDP_ASSOCIATE);
+            flow_data->set_socks_version(SOCKS5_VERSION);
+            flow_data->set_initiator(SOCKS_INITIATOR_CLIENT);
+        }
+
+        if ( flow_data->get_state() == SOCKS_STATE_ESTABLISHED and
+            flow_data->get_command() == SOCKS_CMD_UDP_ASSOCIATE )
+        {
+            process_udp_associate_data(p, flow_data);
+        }
+        return;
+    }
+
+    if ( flow_data->get_initiator() == SOCKS_INITIATOR_SERVER )
+    {
+        // Reverse flow (BIND reverse connection) - server initiated SOCKS
+        if ( p->is_from_client() )
+        {
+            flow_data->set_direction(SOCKS_DIR_CLIENT_TO_SERVER);
+            process_reverse_client_data(p, flow_data);
+        }
+        else
+        {
+            flow_data->set_direction(SOCKS_DIR_SERVER_TO_CLIENT);
+            process_reverse_server_data(p, flow_data);
+        }
+    }
+    else
+    {
+        if ( p->is_from_client() )
+        {
+            flow_data->set_direction(SOCKS_DIR_CLIENT_TO_SERVER);
+            process_client_data(p, flow_data);
+        }
+        else
+        {
+            flow_data->set_direction(SOCKS_DIR_SERVER_TO_CLIENT);
+            process_server_data(p, flow_data);
+        }
+    }
+}
+
+void SocksInspector::clear(Packet*) {}
+
+StreamSplitter* SocksInspector::get_splitter(bool c2s)
+{
+    return new SocksSplitter(c2s);
+}
+
+//-------------------------------------------------------------------------
+// SOCKS4/4a parsing functions
+//-------------------------------------------------------------------------
+
+bool SocksInspector::parse_socks4_request(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+
+    // SOCKS4 request: VER(1) CMD(1) PORT(2) IP(4) USERID(variable) NULL(1)
+    if ( !has_minimum_length(len, SOCKS4_MIN_REQUEST_LEN) or !data )
+        return false;
+
+    const Socks4Request* req = reinterpret_cast<const Socks4Request*>(data);
+
+    if ( !is_valid_socks4_version(req->version) )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    flow_data->set_socks_version(SOCKS4_VERSION);
+
+    if ( !is_valid_command(req->command) )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_UNKNOWN_COMMAND);
+        return false;
+    }
+
+    flow_data->set_command(static_cast<SocksCommand>(req->command));
+
+    if ( req->command == SOCKS_CMD_CONNECT )
+        ++socks_stats.connect_requests;
+    else if ( req->command == SOCKS_CMD_BIND )
+        ++socks_stats.bind_requests;
+
+    uint16_t port = ntohs(req->port);
+    flow_data->set_target_port(port);
+
+    uint32_t ip = ntohl(req->ip);
+
+    // SOCKS4a extension: IP is 0.0.0.x (where x != 0) to indicate domain name follows userid
+    // This allows SOCKS4 clients to send domain names instead of resolved IP addresses
+    bool ip_indicates_socks4a = (ip & 0xFFFFFF00) == 0 and (ip & 0xFF) != 0;
+
+    uint16_t offset = sizeof(Socks4Request);
+
+    if ( offset >= len )
+        return false;
+
+    const uint8_t* userid_start = data + offset;
+    const size_t max_search = std::min(static_cast<size_t>(len - offset), static_cast<size_t>(MAX_USERNAME_LEN + 1));
+    const uint8_t* userid_end = static_cast<const uint8_t*>(memchr(userid_start, 0, max_search));
+
+    if ( !userid_end )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    size_t userid_len = userid_end - userid_start;
+    if ( userid_len > MAX_USERNAME_LEN )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    flow_data->set_userid(std::string(reinterpret_cast<const char*>(userid_start), userid_len));
+    offset += userid_len + 1;  // +1 for null terminator
+
+    // For SOCKS4a, we need room for domain name; for regular SOCKS4, we're done
+    // But we need at least to have consumed all bytes up to and including the null terminator
+    if ( offset > len )
+        return false;
+
+    if ( ip_indicates_socks4a )
+    {
+        std::string domain;
+        if ( !parse_socks4a_domain(data, len, offset, domain) )
+        {
+            const Packet* p = DetectionEngine::get_current_packet();
+            if ( p )
+                DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+            return false;
+        }
+
+        flow_data->set_socks4a(true);
+        flow_data->set_target_address(std::move(domain));
+        flow_data->set_address_type(SOCKS_ATYP_DOMAIN);
+    }
+    else
+    {
+        char ip_str[INET_ADDRSTRLEN];
+        snprintf(ip_str, sizeof(ip_str), "%u.%u.%u.%u",
+                (ip >> 24) & 0xFF, (ip >> 16) & 0xFF,
+                (ip >> 8) & 0xFF, ip & 0xFF);
+
+        flow_data->set_target_address(ip_str);
+        flow_data->set_address_type(SOCKS_ATYP_IPV4);
+
+        SfIp target_ip;
+        // SfIp::set expects network-order bytes for AF_INET; use req->ip directly.
+        target_ip.set(&req->ip, AF_INET);
+        flow_data->set_target_ip(target_ip);
+        
+        // Enable XFF logging for this flow
+        Packet* current_pkt = DetectionEngine::get_current_packet();
+        if ( current_pkt && xtra_target_ip_id )
+            Stream::set_extra_data(current_pkt->flow, current_pkt, xtra_target_ip_id);
+    }
+
+    set_next_state(flow_data, SOCKS_STATE_V4_CONNECT_RESPONSE);
+    flow_data->increment_request_count();
+
+    if ( !flow_data->is_session_counted() )
+    {
+        ++socks_stats.sessions;
+        flow_data->set_session_counted(true);
+    }
+
+    return true;
+}
+
+bool SocksInspector::parse_socks4_response(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+
+    if ( !has_minimum_length(len, SOCKS4_RESPONSE_LEN) or !data )
+        return false;
+
+    const Socks4Response* resp = reinterpret_cast<const Socks4Response*>(data);
+
+    // SOCKS4 response version is 0x00, not 0x04!
+    if ( resp->version != 0x00 )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    if ( resp->status == SOCKS4_REP_GRANTED )
+    {
+        // Connection granted
+        // For BIND command, SOCKS4 sends TWO responses:
+        // 1st response: Server is listening (goes to BIND_SECOND_RESPONSE state)
+        // 2nd response: Connection established (goes to ESTABLISHED state)
+        if ( flow_data->get_command() == SOCKS_CMD_BIND and 
+             flow_data->get_state() == SOCKS_STATE_V4_CONNECT_RESPONSE )
+        {
+            set_next_state(flow_data, SOCKS_STATE_V4_BIND_SECOND_RESPONSE);
+        }
+        else
+        {
+            set_next_state(flow_data, SOCKS_STATE_ESTABLISHED);
+            ++socks_stats.successful_connections;
+
+            if ( flow_data->get_command() == SOCKS_CMD_CONNECT )
+                flow_data->set_handoff_pending(true);
+
+            Packet* p = DetectionEngine::get_current_packet();
+            if ( p and p->flow )
+            {
+                SocksTunnelEvent event(flow_data, true);
+                DataBus::publish(socks_pub_id, SocksEventIds::SOCKS_TUNNEL_ESTABLISHED, event, p->flow);
+
+                if ( PacketTracer::is_active() )
+                    PacketTracer::log("SOCKS: SOCKS4 tunnel established, cmd=%u, target=%s:%u\n",
+                        flow_data->get_command(), flow_data->get_target_address().c_str(),
+                        flow_data->get_target_port());
+            }
+
+            handle_protocol_handoff(flow_data);
+        }
+
+        flow_data->increment_response_count();
+
+        uint32_t bind_addr = ntohl(resp->ip);
+        uint16_t bind_port = ntohs(resp->port);
+
+        char addr_str[INET_ADDRSTRLEN];
+        struct in_addr addr;
+        addr.s_addr = htonl(bind_addr);
+        inet_ntop(AF_INET, &addr, addr_str, INET_ADDRSTRLEN);
+
+        // Store in bind fields, NOT target fields (target was set from request)
+        flow_data->set_bind_address(std::string(addr_str));
+        flow_data->set_bind_port(bind_port);
+        flow_data->set_bind_address_type(SOCKS_ATYP_IPV4);
+        return true;
+    }
+    else
+    {
+        set_next_state(flow_data, SOCKS_STATE_ERROR);
+        flow_data->set_last_error(static_cast<SocksReplyCode>(resp->status));
+        ++socks_stats.failed_connections;
+
+        Packet* p = DetectionEngine::get_current_packet();
+        if ( p and p->flow )
+        {
+            SocksTunnelEvent event(flow_data, false);
+            DataBus::publish(socks_pub_id, SocksEventIds::SOCKS_TUNNEL_FAILED, event, p->flow);
+
+            if ( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS: SOCKS4 tunnel failed, status=0x%02x, target=%s:%u\n",
+                    resp->status, flow_data->get_target_address().c_str(),
+                    flow_data->get_target_port());
+        }
+
+        return false;
+    }
+}
+
+bool SocksInspector::parse_socks4a_domain(const uint8_t* data, uint16_t len, uint16_t& offset, std::string& domain)
+{
+    domain.clear();
+
+    const uint8_t* domain_start = data + offset;
+    const size_t max_search = std::min(static_cast<size_t>(len - offset), static_cast<size_t>(RFC1035_MAX_DOMAIN_LEN + 1));
+    const uint8_t* domain_end = static_cast<const uint8_t*>(memchr(domain_start, 0, max_search));
+
+    if ( !domain_end )
+        return false;
+
+    size_t domain_len = domain_end - domain_start;
+
+    if ( domain_len == 0 or domain_len > RFC1035_MAX_DOMAIN_LEN )
+        return false;
+
+    domain.assign(reinterpret_cast<const char*>(domain_start), domain_len);
+    offset += domain_len;
+
+    return true;
+}
+
+//-------------------------------------------------------------------------
+// SOCKS5 parsing functions
+//-------------------------------------------------------------------------
+
+bool SocksInspector::parse_socks5_auth_negotiation(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+
+    if ( !has_minimum_length(len, SOCKS5_AUTH_NEG_MIN_LEN) or !data )
+        return false;
+
+    const Socks5AuthNegotiation* auth_neg = reinterpret_cast<const Socks5AuthNegotiation*>(data);
+
+    if ( !is_valid_socks5_version(auth_neg->version) )
+        return false;
+
+    if ( auth_neg->num_methods == 0 or len < (SOCKS5_AUTH_RESPONSE_LEN + auth_neg->num_methods) )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    flow_data->set_socks_version(SOCKS5_VERSION);
+    flow_data->increment_request_count();
+    set_next_state(flow_data, SOCKS_STATE_V5_AUTH_NEGOTIATION);
+
+    ++socks_stats.auth_requests;
+
+
+    if ( !flow_data->is_session_counted() )
+    {
+        ++socks_stats.sessions;
+        flow_data->set_session_counted(true);
+    }
+
+    return true;
+}
+
+bool SocksInspector::parse_socks5_auth_response(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+
+    if ( !has_minimum_length(len, SOCKS5_AUTH_RESPONSE_LEN)  or !data )
+        return false;
+
+    const Socks5AuthResponse* auth_resp = reinterpret_cast<const Socks5AuthResponse*>(data);
+    
+    if ( !is_valid_socks5_version(auth_resp->version) )
+        return false;
+
+    flow_data->set_auth_method(static_cast<Socks5AuthMethod>(auth_resp->method));
+    flow_data->increment_response_count();
+
+    if ( !flow_data->is_session_counted() and flow_data->get_initiator() == SOCKS_INITIATOR_SERVER )
+    {
+        ++socks_stats.sessions;
+        flow_data->set_session_counted(true);
+    }
+
+    if ( auth_resp->method == SOCKS5_AUTH_NO_ACCEPTABLE )
+    {
+        set_next_state(flow_data, SOCKS_STATE_ERROR);
+        ++socks_stats.auth_failures;
+        return false;
+    }
+    else if ( auth_resp->method == SOCKS5_AUTH_USERNAME_PASSWORD )
+        set_next_state(flow_data, SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH);
+    else if ( auth_resp->method == SOCKS5_AUTH_NONE )
+    {
+        set_next_state(flow_data, SOCKS_STATE_V5_CONNECT_REQUEST);
+        ++socks_stats.auth_successes;
+    }
+    else
+    {
+        // Unsupported auth method (e.g., GSSAPI 0x01, or private methods 0x80-0xFE)
+        // We can't parse the auth exchange, but we can still parse the subsequent
+        // CONNECT/BIND/UDP_ASSOCIATE request to extract tunnel metadata
+        set_next_state(flow_data, SOCKS_STATE_V5_CONNECT_REQUEST);
+        
+        if ( PacketTracer::is_active() )
+            PacketTracer::log("SOCKS: Unsupported auth method 0x%02x, skipping auth phase\n", 
+                              auth_resp->method);
+    }
+
+    return true;
+}
+
+bool SocksInspector::validate_socks5_request_header(const Socks5ConnectRequest* conn_req)
+{
+    if ( !conn_req or !is_valid_socks5_version(conn_req->version) )
+        return false;
+
+    if ( !is_valid_command(conn_req->command) )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_UNKNOWN_COMMAND);
+        return false;
+    }
+
+    // RFC 1928: reserved byte should be 0x00, but warn instead of reject
+    if ( conn_req->reserved != 0x00 )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+    }
+
+    // Built-in rule: Unknown address type
+    if ( !is_valid_address_type(conn_req->address_type) )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS5_EVENT_UNKNOWN_ADDRESS_TYPE);
+        return false;
+    }
+
+    return true;
+}
+
+bool SocksInspector::parse_socks5_command_request(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+
+    if ( !has_minimum_length(len, SOCKS5_CONNECT_MIN_LEN) or !data )
+        return false;
+
+    const Socks5ConnectRequest* conn_req = reinterpret_cast<const Socks5ConnectRequest*>(data);
+
+    if ( !validate_socks5_request_header(conn_req) )
+        return false;
+
+    SocksCommand cmd = static_cast<SocksCommand>(conn_req->command);
+    flow_data->set_command(cmd);
+    flow_data->set_address_type(static_cast<SocksAddressType>(conn_req->address_type));
+
+    if ( cmd == SOCKS_CMD_CONNECT )
+        ++socks_stats.connect_requests;
+    else if ( cmd == SOCKS_CMD_BIND )
+        ++socks_stats.bind_requests;
+    else if ( cmd == SOCKS_CMD_UDP_ASSOCIATE )
+        ++socks_stats.udp_associate_requests;
+
+    uint16_t offset = sizeof(Socks5ConnectRequest);
+    SocksAddressType addr_type;
+    std::string address;
+    uint16_t port;
+
+    if ( !parse_socks5_address(data, len, offset, addr_type, address, port) )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    flow_data->set_target_address(address);
+    flow_data->set_target_port(port);
+    
+    if ( addr_type == SOCKS_ATYP_IPV4 or addr_type == SOCKS_ATYP_IPV6 )
+    {
+        SfIp target_ip;
+        if ( target_ip.set(address.c_str()) == SFIP_SUCCESS )
+        {
+            flow_data->set_target_ip(target_ip);
+            
+            // Enable XFF logging for this flow
+            Packet* current_pkt = DetectionEngine::get_current_packet();
+            if ( current_pkt && xtra_target_ip_id )
+                Stream::set_extra_data(current_pkt->flow, current_pkt, xtra_target_ip_id);
+        }
+    }
+
+    flow_data->increment_request_count();
+    set_next_state(flow_data, SOCKS_STATE_V5_CONNECT_RESPONSE);
+    return true;
+}
+
+bool SocksInspector::parse_socks5_address(const uint8_t* data, uint16_t len, uint16_t& offset,
+                                          SocksAddressType& addr_type, std::string& address, uint16_t& port)
+{
+    if ( !data or len == 0 )
+        return false;
+
+    if ( offset == 0 or offset > len or offset - 1 >= len )
+        return false;
+
+    addr_type = static_cast<SocksAddressType>(data[offset - 1]); // Address type is at offset-1
+
+    switch (addr_type)
+    {
+        case SOCKS_ATYP_IPV4:
+        {
+            if ( len - offset < IPV4_ADDR_LEN + PORT_LEN ) // 4 bytes IP + 2 bytes port (safe from overflow)
+                return false;
+
+            struct in_addr ipv4_addr;
+            memcpy(&ipv4_addr, data + offset, IPV4_ADDR_LEN);
+            char ip_str[INET_ADDRSTRLEN];
+            inet_ntop(AF_INET, &ipv4_addr, ip_str, INET_ADDRSTRLEN);
+            address = ip_str;
+
+            uint16_t port_raw;
+            memcpy(&port_raw, data + offset + IPV4_ADDR_LEN, sizeof(uint16_t));
+            port = ntohs(port_raw);
+            offset += IPV4_ADDR_LEN + PORT_LEN;
+            break;
+        }
+
+        case SOCKS_ATYP_DOMAIN:
+        {
+            if ( offset >= len )
+                return false;
+
+            uint8_t domain_len = data[offset];
+
+            if ( offset > UINT16_MAX - 1 )
+                return false;
+
+            offset++;
+
+            if ( domain_len == 0 or domain_len > RFC1035_MAX_DOMAIN_LEN ) // RFC 1035 max domain length
+                return false;
+
+            if ( len - offset < domain_len + PORT_LEN ) // domain + 2 bytes port
+                return false;
+            
+            address = std::string(reinterpret_cast<const char*>(data + offset), domain_len);
+
+            if ( offset > UINT16_MAX - domain_len - PORT_LEN )
+                return false;
+
+            offset += domain_len;
+
+            uint16_t port_raw;
+            memcpy(&port_raw, data + offset, sizeof(uint16_t));
+            port = ntohs(port_raw);
+            offset += PORT_LEN;
+            break;
+        }
+
+        case SOCKS_ATYP_IPV6:
+        {
+            if ( len - offset < IPV6_ADDR_LEN + PORT_LEN ) // 16 bytes IP + 2 bytes port (safe from overflow)
+                return false;
+
+            struct in6_addr ipv6_addr;
+            memcpy(&ipv6_addr, data + offset, IPV6_ADDR_LEN);
+            char ip_str[INET6_ADDRSTRLEN];
+            inet_ntop(AF_INET6, &ipv6_addr, ip_str, INET6_ADDRSTRLEN);
+            address = ip_str;
+
+            uint16_t port_raw;
+            memcpy(&port_raw, data + offset + IPV6_ADDR_LEN, sizeof(uint16_t));
+            port = ntohs(port_raw);
+            offset += IPV6_ADDR_LEN + PORT_LEN;
+            break;
+        }
+
+        default:
+            return false;
+    }
+
+    return true;
+}
+
+bool SocksInspector::is_valid_socks4_version(uint8_t version)
+{ return version == SOCKS4_VERSION; }
+
+bool SocksInspector::is_valid_socks5_version(uint8_t version)
+{ return version == SOCKS5_VERSION; }
+
+uint8_t SocksInspector::detect_socks_version(const uint8_t* data, uint16_t len)
+{
+    if (len < 1)
+        return 0;
+
+    uint8_t version = data[0];
+    if (version == SOCKS4_VERSION or version == SOCKS5_VERSION)
+        return version;
+
+    return 0;
+}
+
+bool SocksInspector::is_valid_command(uint8_t command)
+{ return command >= SOCKS_CMD_CONNECT and command <= SOCKS_CMD_UDP_ASSOCIATE; }
+
+bool SocksInspector::is_valid_address_type(uint8_t addr_type)
+{
+    return addr_type == SOCKS_ATYP_IPV4 or
+           addr_type == SOCKS_ATYP_DOMAIN or
+           addr_type == SOCKS_ATYP_IPV6;
+}
+
+SocksFlowData* SocksInspector::get_flow_data(const Flow* flow)
+{ return static_cast<SocksFlowData*>(flow->get_flow_data(SocksFlowData::get_inspector_id())); }
+
+void SocksInspector::create_flow_data(Flow* flow)
+{
+    SocksFlowData* flow_data = new SocksFlowData();
+    flow->set_flow_data(flow_data);
+}
+
+void SocksInspector::process_client_data(Packet* p, SocksFlowData* flow_data)
+{
+    const uint8_t* data = p->data;
+    uint16_t len = p->dsize;
+
+    switch ( flow_data->get_state() )
+    {
+        case SOCKS_STATE_INIT:
+        {
+            uint8_t version = detect_socks_version(data, len);
+            
+            if (version == SOCKS4_VERSION)
+                parse_socks4_request(data, len, flow_data);
+            else if (version == SOCKS5_VERSION)
+                parse_socks5_auth_negotiation(data, len, flow_data);
+            break;
+        }
+
+        case SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH:
+            parse_socks5_username_password_auth(data, len, flow_data);
+            break;
+
+        case SOCKS_STATE_V5_CONNECT_REQUEST:
+            parse_socks5_command_request(data, len, flow_data);
+            break;
+
+        case SOCKS_STATE_ESTABLISHED:
+            process_tunneled_data(p, flow_data);
+            break;
+
+        case SOCKS_STATE_ERROR:
+            // State is already ERROR, skip redundant check in handle_error_state
+            {
+                if ( len == 0 or (data[0] != SOCKS4_VERSION and data[0] != SOCKS5_VERSION) )
+                {
+                    trigger_service_detection(p, flow_data);
+                    flow_data->set_handoff_completed(true);
+                }
+            }
+            break;
+
+        default:
+            break;
+    }
+}
+
+void SocksInspector::process_server_data(Packet* p, SocksFlowData* flow_data)
+{
+    const uint8_t* data = p->data;
+    uint16_t len = p->dsize;
+
+    switch ( flow_data->get_state() )
+    {
+        case SOCKS_STATE_V4_CONNECT_RESPONSE:
+            parse_socks4_response(data, len, flow_data);
+            break;
+
+        case SOCKS_STATE_V4_BIND_SECOND_RESPONSE:
+            parse_socks4_response(data, len, flow_data);
+            break;
+
+        case SOCKS_STATE_V5_AUTH_NEGOTIATION:
+            parse_socks5_auth_response(data, len, flow_data);
+            break;
+
+        case SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH:
+            parse_socks5_username_password_auth_resp(data, len, flow_data);
+            break;
+
+        case SOCKS_STATE_V5_CONNECT_RESPONSE:
+            parse_socks5_command_response(data, len, flow_data);
+            break;
+
+        case SOCKS_STATE_ESTABLISHED:
+            process_tunneled_data(p, flow_data);
+            break;
+
+        case SOCKS_STATE_ERROR:
+            {
+                if ( len == 0 or (data[0] != SOCKS4_VERSION and data[0] != SOCKS5_VERSION) )
+                {
+                    trigger_service_detection(p, flow_data);
+                    flow_data->set_handoff_completed(true);
+                }
+            }
+            break;
+            
+        default:
+            break;
+    }
+}
+
+bool SocksInspector::has_minimum_length(uint16_t data_len, uint16_t required_len)
+{ return data_len >= required_len; }
+
+void SocksInspector::set_next_state(SocksFlowData* flow_data, SocksState new_state)
+{
+    flow_data->set_state(new_state);
+}
+
+void SocksInspector::process_tunneled_data(Packet* p, SocksFlowData* flow_data)
+{
+
+    if ( flow_data->get_command() == SOCKS_CMD_CONNECT and flow_data->is_handoff_pending() )
+    {
+        trigger_service_detection(p, flow_data);
+        return;
+    }
+}
+
+void SocksInspector::process_udp_associate_data(Packet* p, SocksFlowData* flow_data)
+{
+    // Parse SOCKS5-UDP header (RFC 1928 Section 7)
+    // +----+------+------+----------+----------+----------+
+    // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
+    // +----+------+------+----------+----------+----------+
+    // | 2  |  1   |  1   | Variable |    2     | Variable |
+    // +----+------+------+----------+----------+----------+
+
+    if ( p->dsize < SOCKS5_UDP_IPV4_HEADER )
+        return;
+
+    const uint8_t* data = p->data;
+
+    // Parse header fields
+    uint16_t rsv = (data[0] << 8) | data[1];
+    uint8_t frag_byte = data[SOCKS5_UDP_RSV_LEN];
+    uint8_t atyp = data[SOCKS5_UDP_RSV_LEN + SOCKS5_UDP_FRAG_LEN];
+
+    if ( rsv != 0 )
+    {
+        DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return;
+    }
+
+    // RFC 1928 ยง7: Parse FRAG field
+    // Low 7 bits (0x7F) = fragment position (0 = standalone, 1-127 = fragment position)
+    uint8_t frag_pos = frag_byte & 0x7F;
+
+    socks_stats.udp_packets++;
+
+    // Validate ATYP field for minimum required length
+    if ( atyp == SOCKS_ATYP_IPV6 )
+    {
+        if ( p->dsize < SOCKS5_UDP_IPV6_HEADER )  // RSV(2) + FRAG(1) + ATYP(1) + IPv6(16) + PORT(2)
+            return;
+    }
+    else if ( atyp == SOCKS_ATYP_DOMAIN )
+    {
+        uint8_t domain_len = data[SOCKS5_UDP_HEADER_BASE];
+        if ( p->dsize < SOCKS5_UDP_DOMAIN_HEADER_MIN + domain_len )  // RSV(2) + FRAG(1) + ATYP(1) + LEN(1) + DOMAIN + PORT(2)
+            return;
+    }
+    else if ( atyp != SOCKS_ATYP_IPV4 )
+        return;
+    // IPv4 already validated by initial p->dsize < 10 check
+    // RFC 1928 ยง7: Handle fragmentation
+    // frag_pos=0: Standalone packet (no fragmentation)
+
+    if ( frag_pos == 0 )
+    {
+        // Standalone packet (no fragmentation) - create pseudo packet for inner protocol inspection
+        uint16_t header_len = SOCKS5_UDP_HEADER_BASE;  // RSV(2) + FRAG(1) + ATYP(1)
+
+        if ( atyp == SOCKS_ATYP_IPV4 )
+            header_len = SOCKS5_UDP_IPV4_HEADER;  // RSV(2) + FRAG(1) + ATYP(1) + IPv4(4) + PORT(2)
+        else if ( atyp == SOCKS_ATYP_IPV6 )
+            header_len = SOCKS5_UDP_IPV6_HEADER;  // RSV(2) + FRAG(1) + ATYP(1) + IPv6(16) + PORT(2)
+        else if ( atyp == SOCKS_ATYP_DOMAIN )
+        {
+            uint8_t domain_len = data[SOCKS5_UDP_HEADER_BASE];
+            header_len = SOCKS5_UDP_DOMAIN_HEADER_MIN + domain_len;  // RSV(2) + FRAG(1) + ATYP(1) + LEN(1) + DOMAIN + PORT(2)
+        }
+
+        if ( p->dsize <= header_len )
+            return;
+        const uint8_t* payload = data + header_len;
+        uint16_t payload_len = p->dsize - header_len;
+
+        SfIp dst_ip;
+        uint16_t dst_port = 0;
+
+        std::string target_address;
+        
+        if ( atyp == SOCKS_ATYP_IPV4 )
+        {
+            dst_ip.set(&data[SOCKS5_UDP_HEADER_BASE], AF_INET);
+            dst_port = (data[SOCKS5_UDP_HEADER_BASE + IPV4_ADDR_LEN] << 8) | data[SOCKS5_UDP_HEADER_BASE + IPV4_ADDR_LEN + 1];
+            
+            char ip_str[INET6_ADDRSTRLEN];
+            inet_ntop(AF_INET, &data[SOCKS5_UDP_HEADER_BASE], ip_str, INET_ADDRSTRLEN);
+            target_address = ip_str;
+            
+            if( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS5-UDP: Parsed IPv4 dst=%s:%u \n", dst_ip.ntop(ip_str), dst_port);
+        }
+        else if ( atyp == SOCKS_ATYP_IPV6 )
+        {
+            dst_ip.set(&data[SOCKS5_UDP_HEADER_BASE], AF_INET6);
+            dst_port = (data[SOCKS5_UDP_HEADER_BASE + IPV6_ADDR_LEN] << 8) | data[SOCKS5_UDP_HEADER_BASE + IPV6_ADDR_LEN + 1];
+            
+            char ip_str[INET6_ADDRSTRLEN];
+            inet_ntop(AF_INET6, &data[SOCKS5_UDP_HEADER_BASE], ip_str, INET6_ADDRSTRLEN);
+            target_address = ip_str;
+        }
+        else if ( atyp == SOCKS_ATYP_DOMAIN )
+        {
+            uint8_t domain_len = data[SOCKS5_UDP_HEADER_BASE];
+            target_address.assign(reinterpret_cast<const char*>(&data[SOCKS5_UDP_HEADER_BASE + 1]), domain_len);
+            dst_port = (data[SOCKS5_UDP_HEADER_BASE + 1 + domain_len] << 8) | data[SOCKS5_UDP_HEADER_BASE + 1 + domain_len + 1];
+        }
+        
+        // Populate flow data with UDP packet metadata for IPS options
+        flow_data->set_address_type(static_cast<SocksAddressType>(atyp));
+        flow_data->set_target_address(target_address);
+        flow_data->set_target_port(dst_port);
+        
+        // ATYP=DOMAIN: metadata only, no pseudo-packet (can't build IP header without resolved IP)
+        if ( atyp == SOCKS_ATYP_DOMAIN )
+            return;
+
+        // Determine packet direction for correct src/dst assignment in pseudo-packet
+        // Clientโ†’Server: src=client_ip, dst=dst_ip (from SOCKS header)
+        // Serverโ†’Client: src=dst_ip (from SOCKS header), dst=client_ip
+        const bool from_client = p->is_from_client();
+
+        // Build pseudo packet - check if parent has Ethernet header or is raw IP
+        // Match the parent packet's link layer format by checking if packet has Ethernet layer
+        const bool has_eth = (p->num_layers > 0 and
+            (p->layers[0].prot_id == ProtocolId::ETHERNET_802_3 or
+             p->layers[0].prot_id == ProtocolId::ETHERNET_802_11 or
+             p->layers[0].prot_id == ProtocolId::ETHERNET_LLC));
+        const uint32_t eth_len = has_eth ? sizeof(eth::EtherHdr) : 0;
+        const uint32_t ip_len = dst_ip.is_ip4() ? sizeof(ip::IP4Hdr) : sizeof(ip::IP6Hdr);
+        const uint32_t udp_len = sizeof(udp::UDPHdr);
+        const uint32_t total_len = eth_len + ip_len + udp_len + payload_len;
+
+        std::unique_ptr<uint8_t[]> pkt_data(new uint8_t[total_len]);
+        memset(pkt_data.get(), 0, total_len);
+
+        uint32_t offset = 0;
+
+        if ( has_eth )
+        {
+            eth::EtherHdr* eth_hdr = reinterpret_cast<eth::EtherHdr*>(pkt_data.get());
+            eth_hdr->ether_type = htons(dst_ip.is_ip4() ? 0x0800 : 0x86DD);
+            offset = eth_len;
+        }
+
+        if ( dst_ip.is_ip4() )
+        {
+            ip::IP4Hdr* ip4_hdr = reinterpret_cast<ip::IP4Hdr*>(pkt_data.get() + offset);
+            ip4_hdr->ip_verhl = 0x45;  // Version 4, header length 5 (20 bytes)
+            ip4_hdr->ip_len = htons(ip_len + udp_len + payload_len);
+            ip4_hdr->ip_ttl = 64;
+            ip4_hdr->ip_proto = static_cast<IpProtocol>(IPPROTO_UDP);
+
+            // Set src/dst based on direction
+            if ( from_client )
+            {
+                // Clientโ†’Server: src=client, dst=target from SOCKS header
+                if ( p->flow->client_ip.is_ip4() )
+                    memcpy(&ip4_hdr->ip_src, p->flow->client_ip.get_ip4_ptr(), IPV4_ADDR_LEN);
+                else
+                    memset(&ip4_hdr->ip_src, 0, IPV4_ADDR_LEN);
+                memcpy(&ip4_hdr->ip_dst, dst_ip.get_ip4_ptr(), IPV4_ADDR_LEN);
+            }
+            else
+            {
+                // Serverโ†’Client: src=target from SOCKS header, dst=client
+                memcpy(&ip4_hdr->ip_src, dst_ip.get_ip4_ptr(), IPV4_ADDR_LEN);
+                if ( p->flow->client_ip.is_ip4() )
+                    memcpy(&ip4_hdr->ip_dst, p->flow->client_ip.get_ip4_ptr(), IPV4_ADDR_LEN);
+                else
+                    memset(&ip4_hdr->ip_dst, 0, IPV4_ADDR_LEN);
+            }
+
+            offset += ip_len;
+        }
+        else
+        {
+            ip::IP6Hdr* ip6_hdr = reinterpret_cast<ip::IP6Hdr*>(pkt_data.get() + offset);
+            ip6_hdr->ip6_vtf = htonl(0x60000000);  // Version 6
+            ip6_hdr->ip6_payload_len = htons(udp_len + payload_len);
+            ip6_hdr->ip6_next = static_cast<IpProtocol>(IPPROTO_UDP);
+            ip6_hdr->ip6_hoplim = 64;
+
+            // Set src/dst based on direction
+            if ( from_client )
+            {
+                // Clientโ†’Server: src=client, dst=target from SOCKS header
+                if ( p->flow->client_ip.is_ip6() )
+                    memcpy(&ip6_hdr->ip6_src, p->flow->client_ip.get_ip6_ptr(), IPV6_ADDR_LEN);
+                else
+                    memset(&ip6_hdr->ip6_src, 0, IPV6_ADDR_LEN);
+                memcpy(&ip6_hdr->ip6_dst, dst_ip.get_ip6_ptr(), IPV6_ADDR_LEN);
+            }
+            else
+            {
+                // Serverโ†’Client: src=target from SOCKS header, dst=client
+                memcpy(&ip6_hdr->ip6_src, dst_ip.get_ip6_ptr(), IPV6_ADDR_LEN);
+                if ( p->flow->client_ip.is_ip6() )
+                    memcpy(&ip6_hdr->ip6_dst, p->flow->client_ip.get_ip6_ptr(), IPV6_ADDR_LEN);
+                else
+                    memset(&ip6_hdr->ip6_dst, 0, IPV6_ADDR_LEN);
+            }
+            
+            offset += ip_len;
+        }
+
+        udp::UDPHdr* udp_hdr = reinterpret_cast<udp::UDPHdr*>(pkt_data.get() + offset);
+        // Set ports based on direction
+        if ( from_client )
+        {
+            udp_hdr->uh_sport = htons(p->flow->client_port ? p->flow->client_port : 0);
+            udp_hdr->uh_dport = htons(dst_port);
+        }
+        else
+        {
+            udp_hdr->uh_sport = htons(dst_port);
+            udp_hdr->uh_dport = htons(p->flow->client_port ? p->flow->client_port : 0);
+        }
+        udp_hdr->uh_len = htons(udp_len + payload_len);
+        udp_hdr->uh_chk = 0;
+
+        offset += udp_len;
+
+        memcpy(pkt_data.get() + offset, payload, payload_len);
+
+        if( PacketTracer::is_active() )
+            PacketTracer::log("SOCKS5-UDP: Rebuilt packet created, total_len=%u, ip_ver=%s, has_eth=%d, from_client=%d", 
+                total_len, dst_ip.is_ip4() ? "4" : "6", has_eth, from_client);
+
+        // Clear service/inspectors BEFORE creating pseudo packet so wizard can detect inner protocol
+        const char* saved_service = p->flow->service;
+        Inspector* saved_clouseau = p->flow->clouseau;
+        Inspector* saved_gadget = p->flow->gadget;
+
+        p->flow->service = nullptr;
+        p->flow->clouseau = nullptr;
+        p->flow->gadget = nullptr;
+
+        Packet* pseudo_pkt = DetectionEngine::set_next_packet(p, p->flow);
+
+        DAQ_PktHdr_t pkth;
+        memset(&pkth, 0, sizeof(pkth));
+        pkth.ts.tv_sec = p->pkth->ts.tv_sec;
+        pkth.ts.tv_usec = p->pkth->ts.tv_usec;
+        pkth.pktlen = total_len;
+
+        DetectionEngine de;
+        de.set_encode_packet(const_cast<Packet*>(p));
+
+        Analyzer::get_local_analyzer()->process_rebuilt_packet(
+            pseudo_pkt, &pkth, pkt_data.get(), total_len);
+
+        de.set_encode_packet(nullptr);
+
+        p->flow->service = saved_service;
+        p->flow->clouseau = saved_clouseau;
+        p->flow->gadget = saved_gadget;
+
+    }
+    else
+    {
+        // Fragmented packet (frag_pos 1-127)
+        // RFC 1928 ยง7: Fragmentation is rarely used in practice
+        // UDP reassembly not supported - generate alert and stop inspection on this flow
+        DetectionEngine::queue_event(GID_SOCKS, SOCKS5_EVENT_UDP_FRAGMENTATION);
+
+        p->flow->set_to_client_detection(false);
+        p->flow->set_to_server_detection(false);
+
+        if ( config->get_config()->block_udp_fragmentation )
+        {
+            if ( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS: Blocking flow due to UDP fragmentation (FRAG > 0) (config enabled)\n");
+
+            ++socks_stats.udp_frags_blocked;
+            DetectionEngine::disable_all(p);
+            p->active->block_session(p, true);
+            p->active->set_drop_reason("socks_udp_frag");
+        }
+        else
+        {
+            if ( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS: Dropping UDP fragment (FRAG > 0), Fragmentation not supported\n");
+
+            ++socks_stats.udp_frags_dropped;
+        }
+
+
+        return;
+    }
+}
+
+bool SocksInspector::parse_socks5_username_password_auth(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+    if ( !has_minimum_length(len, SOCKS5_AUTH_NEG_MIN_LEN) or !data ) // version + username_len + minimum username
+        return false;
+
+    // Parse username/password authentication request (RFC 1929)
+    if ( data[0] != 0x01 ) // Subnegotiation version must be 0x01
+        return false;
+
+    uint8_t username_len = data[1];
+    if ( username_len == 0 )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    if ( username_len > len - 3 )
+        return false;
+
+    uint8_t password_len = data[2 + username_len];
+    if ( password_len > len - 3 - username_len )
+        return false;
+    flow_data->increment_request_count();
+    set_next_state(flow_data, SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH);
+    return true;
+}
+
+bool SocksInspector::parse_socks5_username_password_auth_resp(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+    if ( !has_minimum_length(len, sizeof(Socks5UsernamePasswordAuthResp)) or !data )
+        return false;
+
+    const Socks5UsernamePasswordAuthResp* auth_resp = reinterpret_cast<const Socks5UsernamePasswordAuthResp*>(data);
+
+    if ( auth_resp->version != 0x01 ) // Subnegotiation version must be 0x01
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        return false;
+    }
+
+    flow_data->increment_response_count();
+
+    if ( auth_resp->status == 0x00 ) // Success
+    {
+        set_next_state(flow_data, SOCKS_STATE_V5_CONNECT_REQUEST);
+        ++socks_stats.auth_successes;
+    }
+    else
+    {
+        set_next_state(flow_data, SOCKS_STATE_ERROR);
+        ++socks_stats.auth_failures;
+        return false;
+    }
+
+    return true;
+}
+
+bool SocksInspector::parse_socks5_command_response(const uint8_t* data, uint16_t len, SocksFlowData* flow_data)
+{
+    if ( !has_minimum_length(len, SOCKS5_CONNECT_MIN_LEN) or !data )
+        return false;
+
+    const Socks5ConnectResponse* conn_resp = reinterpret_cast<const Socks5ConnectResponse*>(data);
+
+    if ( !is_valid_socks5_version(conn_resp->version) )
+        return false;
+
+    // RFC 1928 specifies reserved byte should be 0x00, but some implementations
+    // may not strictly follow this. Log warning but don't reject.
+    if ( conn_resp->reserved != 0x00 )
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+        // Don't return false - continue processing
+    }
+
+    flow_data->set_last_error(static_cast<SocksReplyCode>(conn_resp->reply_code));
+    flow_data->increment_response_count();
+
+    uint16_t offset = 4; // Skip fixed header
+    SocksAddressType addr_type = SOCKS_ATYP_IPV4;
+    std::string bind_address;
+    uint16_t bind_port = 0;
+
+
+    if ( parse_socks5_address(data, len, offset, addr_type, bind_address, bind_port) )
+    {
+        // Store bind address from response (where proxy bound/connected)
+        // This is separate from target (what client requested)
+        flow_data->set_bind_address(bind_address);
+        flow_data->set_bind_port(bind_port);
+        flow_data->set_bind_address_type(addr_type);
+
+        // Note: Do NOT set target_ip from bind_address - they are different concepts:
+        // - target: where client wants to connect (from request)
+        // - bind: where proxy bound/connected (from response)
+    }
+    else
+    {
+        const Packet* p = DetectionEngine::get_current_packet();
+        if ( p )
+            DetectionEngine::queue_event(GID_SOCKS, SOCKS_EVENT_PROTOCOL_VIOLATION);
+    }
+
+    if ( conn_resp->reply_code == SOCKS5_REP_SUCCESS )
+    {
+        set_next_state(flow_data, SOCKS_STATE_ESTABLISHED);
+        ++socks_stats.successful_connections;
+
+        if ( flow_data->get_command() == SOCKS_CMD_CONNECT )
+            flow_data->set_handoff_pending(true);
+        Packet* p = DetectionEngine::get_current_packet();
+
+        if ( flow_data->get_command() == SOCKS_CMD_UDP_ASSOCIATE and p and p->flow )
+        {
+            ++socks_stats.udp_associations_created;
+
+            // Create dynamic UDP expectation for the bind address/port
+            // This allows UDP traffic to the proxy's relay endpoint to be
+            // automatically bound to the SOCKS inspector without wizard patterns
+            if ( bind_port != 0 and (addr_type == SOCKS_ATYP_IPV4 or addr_type == SOCKS_ATYP_IPV6) )
+            {
+                SfIp bind_ip;
+                if ( bind_ip.set(bind_address.c_str()) == SFIP_SUCCESS )
+                {
+                    // Client IP from the TCP control flow
+                    const SfIp* client_ip = &p->flow->client_ip;
+
+                    // Create SocksFlowData for the expected UDP flow
+                    // Pre-configure it as UDP ASSOCIATE so UDP packets are processed correctly
+                    SocksFlowData* udp_fd = new SocksFlowData;
+                    udp_fd->set_state(SOCKS_STATE_ESTABLISHED);
+                    udp_fd->set_command(SOCKS_CMD_UDP_ASSOCIATE);
+                    udp_fd->set_socks_version(SOCKS5_VERSION);
+                    udp_fd->set_initiator(SOCKS_INITIATOR_CLIENT);
+
+                    // Create expectation: client -> bind_ip:bind_port (UDP)
+                    // bidirectional=true so responses also match
+                    int result = Stream::set_snort_protocol_id_expected(
+                        p,                          // control packet (TCP)
+                        PktType::UDP,               // expect UDP packets
+                        IpProtocol::UDP,            // UDP protocol
+                        client_ip,                  // source: SOCKS client
+                        0,                          // any source port
+                        &bind_ip,                   // dest: proxy's UDP relay
+                        bind_port,                  // dest port from response
+                        socks_snort_protocol_id,   // bind to SOCKS inspector
+                        udp_fd,                     // attach pre-configured flow data
+                        false,                      // don't swap direction
+                        false,                      // single expectation
+                        true);                      // bidirectional
+
+                    if ( result >= 0 )
+                        ++socks_stats.udp_expectations_created;
+                    else
+                        delete udp_fd;  // Clean up on failure
+
+                    (void)result;  // Suppress unused variable warning
+                }
+            }
+
+            if ( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS: SOCKS5 UDP ASSOCIATE established, BND.ADDR=%s BND.PORT=%u\n",
+                    bind_address.c_str(), bind_port);
+        }
+        if ( p and p->flow )
+        {
+            SocksTunnelEvent event(flow_data, true);
+            DataBus::publish(socks_pub_id, SocksEventIds::SOCKS_TUNNEL_ESTABLISHED, event, p->flow);
+            
+            if ( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS: SOCKS5 tunnel established, cmd=%u, target=%s:%u\n",
+                    flow_data->get_command(), flow_data->get_target_address().c_str(),
+                    flow_data->get_target_port());
+        }
+
+        handle_protocol_handoff(flow_data);
+    }
+    else
+    {
+        set_next_state(flow_data, SOCKS_STATE_ERROR);
+        ++socks_stats.failed_connections;
+
+        Packet* p = DetectionEngine::get_current_packet();
+        if ( p and p->flow )
+        {
+            SocksTunnelEvent event(flow_data, false);
+            DataBus::publish(socks_pub_id, SocksEventIds::SOCKS_TUNNEL_FAILED, event, p->flow);
+            
+            if ( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS: SOCKS5 tunnel failed, reply_code=0x%02x, target=%s:%u\n",
+                    flow_data->get_last_error(), flow_data->get_target_address().c_str(),
+                    flow_data->get_target_port());
+        }
+    }
+
+    return true;
+}
+
+void SocksInspector::handle_protocol_handoff(SocksFlowData* flow_data)
+{
+    if ( flow_data->is_handoff_completed() )
+        return;
+
+    if ( flow_data->get_command() == SOCKS_CMD_CONNECT )
+    {
+        // Trigger handoff immediately - don't wait for tunneled data
+        // This ensures the next packet goes directly to wizard
+        Packet* current_pkt = DetectionEngine::get_current_packet();
+        if ( current_pkt and current_pkt->flow )
+        {
+            bool is_reverse = (flow_data->get_initiator() == SOCKS_INITIATOR_SERVER);
+
+            current_pkt->flow->set_proxied();
+            current_pkt->flow->set_service(current_pkt, nullptr);
+            current_pkt->flow->set_state(Flow::FlowState::INSPECT);
+
+            DataBus::publish(intrinsic_pub_id, IntrinsicEventIds::FLOW_SERVICE_CHANGE, current_pkt);
+
+            flow_data->set_handoff_pending(false);
+            flow_data->set_handoff_completed(true);
+
+            // Get current PAF positions for wizard to continue scanning from.
+            uint32_t to_server_paf = Stream::get_paf_position(current_pkt->flow, true);
+            uint32_t to_client_paf = Stream::get_paf_position(current_pkt->flow, false);
+
+            // Get wizard splitters for both directions.
+            // For reverse SOCKS, we swap the splitter directions so the wizard sees
+            // the tunneled protocol data correctly. In reverse SOCKS:
+            // - The original TCP client sends HTTP requests (should be c2s for wizard)
+            // - The original TCP server sends HTTP responses (should be s2c for wizard)
+            // But without swap_roles(), the directions are inverted, so we install
+            // the c2s splitter on the s2c tracker and vice versa.
+            StreamSplitter* to_server_splitter = nullptr;
+            StreamSplitter* to_client_splitter = nullptr;
+            if ( current_pkt->flow->clouseau )
+            {
+                if ( is_reverse )
+                {
+                    // Swap splitter directions for reverse SOCKS
+                    to_server_splitter = current_pkt->flow->clouseau->get_splitter(false);  // s2c splitter
+                    to_client_splitter = current_pkt->flow->clouseau->get_splitter(true);   // c2s splitter
+                }
+                else
+                {
+                    to_server_splitter = current_pkt->flow->clouseau->get_splitter(true);
+                    to_client_splitter = current_pkt->flow->clouseau->get_splitter(false);
+                }
+            }
+
+            if ( to_server_splitter )
+                Stream::set_splitter_with_rescan(current_pkt->flow, true, to_server_splitter, to_server_paf);
+
+            if ( to_client_splitter )
+                Stream::set_splitter_with_rescan(current_pkt->flow, false, to_client_splitter, to_client_paf);
+
+            if ( PacketTracer::is_active() )
+                PacketTracer::log("SOCKS: Protocol handoff completed, is_reverse=%d, to_server_paf=%u, to_client_paf=%u, dsize=%u. target = %s:%u\n",
+                    is_reverse, to_server_paf, to_client_paf, current_pkt->dsize,
+                    flow_data->get_target_address().c_str(), flow_data->get_target_port());
+        }
+    }
+}
+
+
+void SocksInspector::trigger_service_detection(Packet* p, SocksFlowData* flow_data)
+{
+    if ( !p or !p->flow or !flow_data )
+        return;
+
+    if ( PacketTracer::is_active() )
+        PacketTracer::log("SOCKS: Triggering service detection for tunneled protocol, target=%s:%u\n", 
+                            flow_data->get_target_address().c_str(), flow_data->get_target_port());
+    p->flow->set_proxied();
+    p->flow->set_service(p, nullptr);
+    p->flow->set_state(Flow::FlowState::INSPECT);
+    if ( p->flow->gadget )
+        p->flow->clear_gadget();
+
+    DataBus::publish(intrinsic_pub_id, IntrinsicEventIds::FLOW_SERVICE_CHANGE, p);
+
+    flow_data->set_handoff_pending(false);
+    flow_data->set_handoff_completed(true);
+}
+
+bool SocksInspector::handle_error_state(Packet* p, SocksFlowData* flow_data)
+{
+    if ( flow_data->get_state() != SOCKS_STATE_ERROR )
+        return false;
+
+    const uint8_t* data = p->data;
+    uint16_t len = p->dsize;
+
+    if ( len == 0 or (data[0] != SOCKS4_VERSION and data[0] != SOCKS5_VERSION) )
+    {
+        trigger_service_detection(p, flow_data);
+        flow_data->set_handoff_completed(true);
+    }
+
+    return true;
+}
+
+void SocksInspector::detect_protocol_initiator(const Packet* p, SocksFlowData* flow_data)
+{
+    const uint8_t* data = p->data;
+    uint16_t len = p->dsize;
+
+    if ( !data or len < 2 )
+        return;
+
+    bool from_client = p->is_from_client();
+
+    // Note: SOCKS5 UDP detection is intentionally NOT implemented here.
+    // The SOCKS5 UDP header pattern (00 00 00 XX) is too generic and would
+    // cause false positives on DNS, QUIC, STUN, RTP, and other UDP protocols.
+    // Risk of false drops/blocks on legitimate traffic is too high.
+    // SOCKS5 UDP ASSOCIATE requires explicit port binding by the user.
+    if ( p->is_udp() )
+        return;
+
+    if ( from_client )
+    {
+        if ( data[0] == SOCKS4_VERSION or data[0] == SOCKS5_VERSION )
+        {
+            flow_data->set_initiator(SOCKS_INITIATOR_CLIENT);
+            return;
+        }
+    }
+
+    if ( !from_client )
+    {
+        if ( data[0] == SOCKS5_VERSION )
+        {
+            flow_data->set_initiator(SOCKS_INITIATOR_SERVER);
+            flow_data->set_socks_version(SOCKS5_VERSION);
+            return;
+        }
+
+        // Note: Reverse SOCKS4 (server-initiated SOCKS4) is not supported.
+        // SOCKS4 responses start with 0x00 which is ambiguous and the reverse
+        // processing path only handles SOCKS5. Omitting detection here to avoid
+        // setting initiator/version for a path that won't be processed.
+    }
+}
+
+// Check if data looks like a SOCKS5 command request: ver=5, cmd=1-3, rsv=0, atyp=1/3/4
+static bool is_socks5_command_request(const uint8_t* data, uint16_t len)
+{
+    return len >= 4 and data[0] == SOCKS5_VERSION and
+           data[1] >= 0x01 and data[1] <= 0x03 and data[2] == 0x00 and
+           (data[3] == 0x01 or data[3] == 0x03 or data[3] == 0x04);
+}
+
+// Check if data looks like a SOCKS5 auth negotiation: ver=5, nmethods > 0
+static bool is_socks5_auth_negotiation(const uint8_t* data, uint16_t len)
+{
+    return len >= 3 and data[1] > 0;
+}
+
+void SocksInspector::process_reverse_server_data(Packet* p, SocksFlowData* flow_data)
+{
+    const uint8_t* data = p->data;
+    uint16_t len = p->dsize;
+
+    // In reverse SOCKS, server sends greeting, auth response, and CONNECT request
+    // (opposite of normal flow where client sends these)
+
+    switch ( flow_data->get_state() )
+    {
+        case SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION:
+            // Server can send either auth response OR connect request
+            // In reverse SOCKS, server may send CONNECT request before client's auth response
+            if ( len == 2 )
+            {
+                // RFC 1928: Auth method selection response (2 bytes)
+                // parse_socks5_auth_response() sets forward flow states, override with reverse states
+                if ( parse_socks5_auth_response(data, len, flow_data) )
+                {
+                    if ( flow_data->get_state() != SOCKS_STATE_ERROR )
+                        set_next_state(flow_data, SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+                }
+            }
+            else if ( is_socks5_command_request(data, len) )
+            {
+                // Server sends CONNECT request
+                if ( parse_socks5_command_request(data, len, flow_data) )
+                    set_next_state(flow_data, SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+            }
+            break;
+
+        case SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE:
+            // Server can send CONNECT request or username/password auth
+            if ( is_socks5_command_request(data, len) )
+            {
+                if ( parse_socks5_command_request(data, len, flow_data) )
+                    set_next_state(flow_data, SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+            }
+            else if ( flow_data->get_auth_method() == SOCKS5_AUTH_USERNAME_PASSWORD )
+            {
+                parse_socks5_username_password_auth(data, len, flow_data);
+            }
+            break;
+
+        case SOCKS_STATE_ESTABLISHED:
+            process_tunneled_data(p, flow_data);
+            break;
+
+        case SOCKS_STATE_ERROR:
+            if ( len == 0 or (data[0] != SOCKS4_VERSION and data[0] != SOCKS5_VERSION) )
+            {
+                trigger_service_detection(p, flow_data);
+                flow_data->set_handoff_completed(true);
+            }
+            break;
+
+        default:
+            // Handle initial packets (auth negotiation, connect request)
+            if ( len < 2 or data[0] != SOCKS5_VERSION )
+                return;
+
+            if ( is_socks5_command_request(data, len) )
+            {
+                if ( parse_socks5_command_request(data, len, flow_data) )
+                    set_next_state(flow_data, SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+            }
+            else if ( is_socks5_auth_negotiation(data, len) )
+            {
+                if ( parse_socks5_auth_negotiation(data, len, flow_data) )
+                    set_next_state(flow_data, SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+            }
+            break;
+    }
+}
+
+// Check if data contains combined auth response + connect response
+// Pattern: 05 XX 05 YY 00 ZZ ... (auth_resp followed by connect_resp)
+static bool is_combined_auth_and_connect_response(const uint8_t* data, uint16_t len)
+{
+    return len >= 12 and data[2] == SOCKS5_VERSION and data[4] == 0x00;
+}
+
+// Check if data looks like a CONNECT response: ver=5, rep, rsv=0, atyp
+static bool is_connect_response(const uint8_t* data, uint16_t len)
+{
+    return len >= 4 and data[0] == SOCKS5_VERSION and data[2] == 0x00;
+}
+
+void SocksInspector::process_reverse_client_data(Packet* p, SocksFlowData* flow_data)
+{
+    const uint8_t* data = p->data;
+    uint16_t len = p->dsize;
+
+    // Handle established tunnel or error states
+    if ( flow_data->get_state() == SOCKS_STATE_ESTABLISHED )
+    {
+        process_tunneled_data(p, flow_data);
+        return;
+    }
+
+    if ( handle_error_state(p, flow_data) )
+        return;
+
+    // Skip any non-SOCKS prefix data (splitter may not have flushed at marker
+    // if initiator wasn't set yet when splitter ran)
+    // Only resync in early handshake states to avoid misinterpreting tunneled data
+    SocksState state = flow_data->get_state();
+    if ( len >= 2 and data[0] != SOCKS5_VERSION and
+         (state == SOCKS_STATE_INIT or
+          state == SOCKS_STATE_V5_AUTH_NEGOTIATION or
+          state == SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION or
+          state == SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE or
+          state == SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST) )
+    {
+        for ( uint16_t i = 1; i + 1 < len; i++ )
+        {
+            if ( data[i] == SOCKS5_VERSION )
+            {
+                data += i;
+                len -= i;
+                break;
+            }
+        }
+    }
+
+    if ( len < 2 or data[0] != SOCKS5_VERSION )
+        return;
+
+    // In reverse SOCKS, client sends auth response and CONNECT response
+    // TCP reassembly may combine multiple messages in one PDU
+
+    // Case 1: Combined auth response + connect response
+    if ( is_combined_auth_and_connect_response(data, len) )
+    {
+        if ( parse_socks5_auth_response(data, 2, flow_data) and
+             flow_data->get_state() != SOCKS_STATE_ERROR )
+        {
+            set_next_state(flow_data, SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+            data += 2;
+            len -= 2;
+        }
+    }
+    // Case 2: Standalone auth response (exactly 2 bytes)
+    else if ( len == 2 )
+    {
+        if ( parse_socks5_auth_response(data, 2, flow_data) and
+             flow_data->get_state() != SOCKS_STATE_ERROR )
+        {
+            set_next_state(flow_data, SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+        }
+        return;
+    }
+
+    // Case 3: CONNECT response
+    if ( is_connect_response(data, len) )
+    {
+        if ( parse_socks5_command_response(data, len, flow_data) and
+             flow_data->get_state() == SOCKS_STATE_ESTABLISHED and
+             flow_data->get_command() == SOCKS_CMD_CONNECT )
+        {
+            flow_data->set_handoff_pending(true);
+            handle_protocol_handoff(flow_data);
+        }
+        return;
+    }
+
+    // Case 4: Username/password auth (if that method was selected)
+    if ( flow_data->get_state() == SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE and
+         flow_data->get_auth_method() == SOCKS5_AUTH_USERNAME_PASSWORD )
+    {
+        parse_socks5_username_password_auth(data, len, flow_data);
+    }
+}
+
+// Helper function to cast IP pointer to mutable bytes for XFF API
+// Note: XFF API requires non-const pointer but data is not actually modified
+static inline uint8_t* ip_to_mutable_bytes(const void* ptr)
+{
+    // Cast away const for API compatibility - data is read-only in practice
+    return static_cast<uint8_t*>(const_cast<void*>(ptr));
+}
+
+// cppcheck-suppress constParameterPointer ; API requirement
+int SocksInspector::get_xtra_target_ip(Flow* flow, uint8_t** buf, uint32_t* len, uint32_t* type)
+{
+    if ( !flow )
+        return 0;
+
+    const auto* fd = static_cast<const SocksFlowData*>(flow->get_flow_data(SocksFlowData::get_inspector_id()));
+    if ( !fd )
+        return 0;
+
+    const auto& target = fd->get_target_address_ref();
+    const auto* target_ip_ptr = target.get_ip();
+    if ( !target_ip_ptr )
+        return 0;
+
+    const auto& target_ip = *target_ip_ptr;
+
+    if ( target_ip.is_ip4() )
+    {
+        *buf = ip_to_mutable_bytes(target_ip.get_ip4_ptr());
+        *len = 4;
+        *type = EVENT_INFO_XFF_IPV4;
+    }
+    else if ( target_ip.is_ip6() )
+    {
+        *buf = ip_to_mutable_bytes(target_ip.get_ip6_ptr());
+        *len = 16;
+        *type = EVENT_INFO_XFF_IPV6;
+    }
+    else
+        return 0;
+
+    return 1;
+}
+
+//-------------------------------------------------------------------------
+// API
+//-------------------------------------------------------------------------
+
+static void socks_init()
+{
+    SocksFlowData::init();
+}
+
+static Module* mod_ctor() { return new SocksModule; }
+static void mod_dtor(Module* m) { delete m; }
+static Inspector* socks_ctor(Module* m) { return new SocksInspector(static_cast<SocksModule*>(m)); }
+static void socks_dtor(Inspector* p) { delete p; }
+
+const InspectApi socks_api =
+{
+    {
+        PT_INSPECTOR,
+        sizeof(InspectApi),
+        INSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        SOCKS_NAME,
+        SOCKS_HELP,
+        mod_ctor,
+        mod_dtor
+    },
+    IT_SERVICE,
+    PROTO_BIT__ANY_PDU,
+    nullptr,
+    "socks",
+    socks_init,
+    nullptr,
+    nullptr,
+    nullptr,
+    socks_ctor,
+    socks_dtor,
+    nullptr,
+    nullptr
+};
+
+extern const BaseApi* ips_socks_version;
+extern const BaseApi* ips_socks_state;
+extern const BaseApi* ips_socks_command;
+extern const BaseApi* ips_socks_address_type;
+extern const BaseApi* ips_socks_remote_address;
+extern const BaseApi* ips_socks_remote_port;
+
+#ifdef BUILDING_SO
+SO_PUBLIC const BaseApi* snort_plugins[] =
+{
+    &socks_api.base,
+    ips_socks_version,
+    ips_socks_state,
+    ips_socks_command,
+    ips_socks_address_type,
+    ips_socks_remote_address,
+    ips_socks_remote_port,
+    nullptr
+};
+#else
+const BaseApi* sin_socks[] =
+{
+    &socks_api.base,
+    ips_socks_version,
+    ips_socks_state,
+    ips_socks_command,
+    ips_socks_address_type,
+    ips_socks_remote_address,
+    ips_socks_remote_port,
+    nullptr
+};
+#endif
diff --git a/src/service_inspectors/socks/socks.h b/src/service_inspectors/socks/socks.h
new file mode 100644 (file)
index 0000000..94a2be5
--- /dev/null
@@ -0,0 +1,102 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks.h author Raza Shafiq <rshafiq@cisco.com>
+
+#ifndef SOCKS_H
+#define SOCKS_H
+
+#include "framework/inspector.h"
+#include "socks_flow_data.h"
+
+class SocksModule;
+
+class SocksInspector : public snort::Inspector
+{
+public:
+    SocksInspector(const SocksModule*);
+    bool configure(snort::SnortConfig*) override;
+    void show(const snort::SnortConfig*) const override;
+    void eval(snort::Packet*) override;
+    void clear(snort::Packet*) override;
+    snort::StreamSplitter* get_splitter(bool) override;
+    static int get_xtra_target_ip(snort::Flow*, uint8_t**, uint32_t*, uint32_t*);
+
+protected:
+    const SocksModule* config;  // Kept for config reload support
+    uint32_t xtra_target_ip_id;
+
+    // SOCKS4/4a parsing methods
+    bool parse_socks4_request(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+    bool parse_socks4_response(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+    bool parse_socks4a_domain(const uint8_t* data, uint16_t len, uint16_t& offset, std::string& domain);
+
+    // SOCKS5 parsing methods
+    bool parse_socks5_auth_negotiation(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+    bool parse_socks5_auth_response(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+    bool parse_socks5_username_password_auth(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+    bool parse_socks5_username_password_auth_resp(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+    bool parse_socks5_command_request(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+    bool parse_socks5_command_response(const uint8_t* data, uint16_t len, SocksFlowData* flow_data);
+
+    // Generic address parsing (works for both versions)
+    bool parse_socks5_address(const uint8_t* data, uint16_t len, uint16_t& offset, 
+                             SocksAddressType& addr_type, std::string& address, uint16_t& port);
+
+    // Flow data management
+    SocksFlowData* get_flow_data(const snort::Flow* flow);
+    void create_flow_data(snort::Flow* flow);
+
+    // State machine processing
+    void process_client_data(snort::Packet* p, SocksFlowData* flow_data);
+    void process_server_data(snort::Packet* p, SocksFlowData* flow_data);
+    void process_tunneled_data(snort::Packet* p, SocksFlowData* flow_data);
+    
+    // Reverse flow processing (BIND reverse connections)
+    void detect_protocol_initiator(const snort::Packet* p, SocksFlowData* flow_data);
+    void process_reverse_client_data(snort::Packet* p, SocksFlowData* flow_data);
+    void process_reverse_server_data(snort::Packet* p, SocksFlowData* flow_data);
+
+    // UDP support
+    void process_udp_associate_data(snort::Packet* p, SocksFlowData* flow_data);
+
+    // Protocol handoff
+    void handle_protocol_handoff(SocksFlowData* flow_data);
+    void trigger_service_detection(snort::Packet* p, SocksFlowData* flow_data);
+    
+    // ERROR state handling
+    bool handle_error_state(snort::Packet* p, SocksFlowData* flow_data);
+
+    // Generic validation methods (work for both v4 and v5)
+    [[nodiscard]] bool is_valid_socks4_version(uint8_t version); // Checks for 0x04
+    [[nodiscard]] bool is_valid_socks5_version(uint8_t version); // Checks for 0x05
+    [[nodiscard]] bool is_valid_command(uint8_t command);
+    [[nodiscard]] bool is_valid_address_type(uint8_t addr_type);
+
+    // Version detection helper
+    uint8_t detect_socks_version(const uint8_t* data, uint16_t len);
+
+    // Utility methods
+    [[nodiscard]] bool has_minimum_length(uint16_t data_len, uint16_t required_len);
+    void set_next_state(SocksFlowData* flow_data, SocksState new_state);
+
+    // Helper methods
+    [[nodiscard]] bool validate_socks5_request_header(const Socks5ConnectRequest* conn_req);
+};
+
+#endif
diff --git a/src/service_inspectors/socks/socks_event.h b/src/service_inspectors/socks/socks_event.h
new file mode 100644 (file)
index 0000000..6097903
--- /dev/null
@@ -0,0 +1,82 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_event.h - author Raza Shafiq <rshafiq@cisco.com>
+
+#ifndef SOCKS_EVENT_H
+#define SOCKS_EVENT_H
+
+#include "framework/data_bus.h"
+#include "sfip/sf_ip.h"
+#include <string>
+
+#include "socks_flow_data.h"
+
+// Event IDs for SOCKS events
+struct SocksEventIds { enum : unsigned { SOCKS_TUNNEL_ESTABLISHED, SOCKS_TUNNEL_FAILED, num_ids }; };
+
+const snort::PubKey socks_pub_key { "socks", SocksEventIds::num_ids };
+
+// SOCKS tunnel event published when tunnel is established or fails
+// Provides access to target destination for logging/correlation
+class SocksTunnelEvent : public snort::DataEvent
+{
+public:
+    SocksTunnelEvent(const SocksFlowData* fd, bool success) : 
+        flow_data(fd), tunnel_success(success) { }
+
+
+    // Target destination information (where client wants to go)
+    const std::string& get_target_address() const
+    {
+        static const std::string empty;
+        return flow_data ? flow_data->get_target_address() : empty;
+    }
+
+    uint16_t get_target_port() const
+    {
+        return flow_data ? flow_data->get_target_port() : 0;
+    }
+
+    const snort::SfIp* get_target_ip() const
+    {
+        return flow_data ? flow_data->get_target_ip() : nullptr;
+    }
+
+    // SOCKS command type
+    SocksCommand get_command() const
+    {
+        return flow_data ? flow_data->get_command() : SOCKS_CMD_CONNECT;
+    }
+
+    // Tunnel establishment status
+    bool is_tunnel_successful() const
+    {
+        return tunnel_success;
+    }
+
+    SocksReplyCode get_reply_code() const
+    {
+        return flow_data ? flow_data->get_last_error() : SOCKS5_REP_SUCCESS;
+    }
+
+private:
+    const SocksFlowData* flow_data;
+    bool tunnel_success;
+};
+
+#endif
diff --git a/src/service_inspectors/socks/socks_flow_data.cc b/src/service_inspectors/socks/socks_flow_data.cc
new file mode 100644 (file)
index 0000000..1706dda
--- /dev/null
@@ -0,0 +1,63 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_flow_data.cc - author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "socks_flow_data.h"
+#include "socks_module.h"
+
+using namespace snort;
+
+unsigned SocksFlowData::inspector_id = 0;
+
+void SocksFlowData::init()
+{
+    if ( inspector_id == 0 )
+        inspector_id = FlowData::create_flow_data_id();
+}
+
+SocksFlowData::SocksFlowData() : FlowData(inspector_id),
+    state(SOCKS_STATE_INIT),
+    direction(SOCKS_DIR_CLIENT_TO_SERVER),
+    initiator(SOCKS_INITIATOR_UNKNOWN),
+    socks_version(0),
+    is_socks4a_protocol(false),
+    auth_method(SOCKS5_AUTH_NONE),
+    command(SOCKS_CMD_CONNECT),
+    target(),
+    bind(),
+    request_count(0),
+    response_count(0),
+    last_error(SOCKS5_REP_SUCCESS),
+    handoff_pending(false),
+    handoff_completed(false),
+    session_counted(false)
+{
+    ++socks_stats.concurrent_sessions;
+    if (socks_stats.concurrent_sessions > socks_stats.max_concurrent_sessions)
+        socks_stats.max_concurrent_sessions = socks_stats.concurrent_sessions;
+}
+
+SocksFlowData::~SocksFlowData() noexcept
+{
+    assert(socks_stats.concurrent_sessions > 0);
+    --socks_stats.concurrent_sessions;
+}
diff --git a/src/service_inspectors/socks/socks_flow_data.h b/src/service_inspectors/socks/socks_flow_data.h
new file mode 100644 (file)
index 0000000..c59baa2
--- /dev/null
@@ -0,0 +1,426 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_flow_data.h - author Raza Shafiq <rshafiq@cisco.com>
+
+#ifndef SOCKS_FLOW_DATA_H
+#define SOCKS_FLOW_DATA_H
+
+#include "flow/flow_data.h"
+#include "sfip/sf_ip.h"
+#include <memory>
+#include <string>
+
+#define SOCKS5_VERSION 0x05
+#define SOCKS4_VERSION 0x04
+#define SOCKS4_RESPONSE_VERSION 0x00  // SOCKS4 responses use 0x00, not 0x04
+
+// Protocol constants
+constexpr uint16_t SOCKS4_MIN_REQUEST_LEN = 9;      // VER(1) + CMD(1) + PORT(2) + IP(4) + NULL(1)
+constexpr uint16_t SOCKS4_RESPONSE_LEN = 8;         // VER(1) + STATUS(1) + PORT(2) + IP(4)
+constexpr uint16_t SOCKS5_AUTH_NEG_MIN_LEN = 3;     // VER(1) + NMETHODS(1) + METHODS(1+)
+constexpr uint16_t SOCKS5_AUTH_RESPONSE_LEN = 2;    // VER(1) + METHOD(1)
+constexpr uint16_t SOCKS5_CONNECT_MIN_LEN = 10;     // VER(1) + CMD(1) + RSV(1) + ATYP(1) + ADDR(4+) + PORT(2)
+constexpr uint8_t RFC1035_MAX_DOMAIN_LEN = 253;     // RFC 1035 maximum domain name length
+constexpr uint8_t MAX_USERNAME_LEN = 255;           // SOCKS5 username/password auth
+constexpr uint8_t MAX_PASSWORD_LEN = 255;           // SOCKS5 username/password auth
+constexpr uint8_t SOCKS5_USERPASS_VERSION = 0x01;   // RFC 1929 username/password subnegotiation
+
+// Address and port sizes
+constexpr uint8_t IPV4_ADDR_LEN = 4;
+constexpr uint8_t IPV6_ADDR_LEN = 16;
+constexpr uint8_t PORT_LEN = 2;
+constexpr uint8_t DOMAIN_LEN_FIELD = 1;
+
+// SOCKS5 UDP header component sizes (RFC 1928 Section 7)
+constexpr uint8_t SOCKS5_UDP_RSV_LEN = 2;
+constexpr uint8_t SOCKS5_UDP_FRAG_LEN = 1;
+constexpr uint8_t SOCKS5_UDP_ATYP_LEN = 1;
+constexpr uint8_t SOCKS5_UDP_HEADER_BASE = SOCKS5_UDP_RSV_LEN + SOCKS5_UDP_FRAG_LEN + SOCKS5_UDP_ATYP_LEN;  // 4 bytes
+constexpr uint8_t SOCKS5_UDP_IPV4_HEADER = SOCKS5_UDP_HEADER_BASE + IPV4_ADDR_LEN + PORT_LEN;  // 10 bytes
+constexpr uint8_t SOCKS5_UDP_IPV6_HEADER = SOCKS5_UDP_HEADER_BASE + IPV6_ADDR_LEN + PORT_LEN;  // 22 bytes
+constexpr uint8_t SOCKS5_UDP_DOMAIN_HEADER_MIN = SOCKS5_UDP_HEADER_BASE + DOMAIN_LEN_FIELD + PORT_LEN;  // 7 bytes (+ domain)
+
+enum Socks5AuthMethod : uint8_t
+{
+    SOCKS5_AUTH_NONE = 0x00,
+    SOCKS5_AUTH_GSSAPI = 0x01,
+    SOCKS5_AUTH_USERNAME_PASSWORD = 0x02,
+    SOCKS5_AUTH_NO_ACCEPTABLE = 0xFF
+};
+
+// Generic SOCKS command codes (same for v4 and v5)
+enum SocksCommand : uint8_t
+{
+    SOCKS_CMD_CONNECT = 0x01,
+    SOCKS_CMD_BIND = 0x02,
+    SOCKS_CMD_UDP_ASSOCIATE = 0x03  // SOCKS5 only
+};
+
+// SOCKS address types (SOCKS5 only, SOCKS4 always uses IPv4)
+enum SocksAddressType : uint8_t
+{
+    SOCKS_ATYP_IPV4 = 0x01,
+    SOCKS_ATYP_DOMAIN = 0x03,
+    SOCKS_ATYP_IPV6 = 0x04
+};
+
+// Generic SOCKS reply codes (includes both SOCKS4 and SOCKS5 wire format values)
+enum SocksReplyCode : uint8_t
+{
+    // SOCKS5 reply codes (RFC 1928)
+    SOCKS5_REP_SUCCESS = 0x00,
+    SOCKS5_REP_GENERAL_FAILURE = 0x01,
+    SOCKS5_REP_NOT_ALLOWED = 0x02,
+    SOCKS5_REP_NETWORK_UNREACHABLE = 0x03,
+    SOCKS5_REP_HOST_UNREACHABLE = 0x04,
+    SOCKS5_REP_CONNECTION_REFUSED = 0x05,
+    SOCKS5_REP_TTL_EXPIRED = 0x06,
+    SOCKS5_REP_COMMAND_NOT_SUPPORTED = 0x07,
+    SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED = 0x08,
+
+    // SOCKS4 reply codes (RFC 1928)
+    SOCKS4_REP_GRANTED = 0x5A,           // Request granted
+    SOCKS4_REP_REJECTED = 0x5B,          // Request rejected or failed
+    SOCKS4_REP_NO_IDENTD = 0x5C,         // Request failed (no identd)
+    SOCKS4_REP_IDENTD_FAILED = 0x5D      // Request failed (identd mismatch)
+};
+
+// Generic SOCKS state machine (handles both v4 and v5)
+enum SocksState : uint8_t
+{
+    SOCKS_STATE_INIT = 0,
+    // SOCKS4/4a states
+    SOCKS_STATE_V4_CONNECT_REQUEST,
+    SOCKS_STATE_V4_CONNECT_RESPONSE,
+    SOCKS_STATE_V4_BIND_SECOND_RESPONSE,
+    // SOCKS5 states (forward flow - client initiates)
+    SOCKS_STATE_V5_AUTH_NEGOTIATION,
+    SOCKS_STATE_V5_AUTH_REQUEST,
+    SOCKS_STATE_V5_AUTH_RESPONSE,
+    SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH,
+    SOCKS_STATE_V5_CONNECT_REQUEST,
+    SOCKS_STATE_V5_CONNECT_RESPONSE,
+    // SOCKS5 BIND reverse states (server initiates)
+    SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION,
+    SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE,
+    SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST,
+    SOCKS_STATE_V5_BIND_REVERSE_CONNECT_RESPONSE,
+    // Common states
+    SOCKS_STATE_ESTABLISHED,
+    SOCKS_STATE_ERROR
+};
+
+enum SocksDirection : uint8_t
+{
+    SOCKS_DIR_CLIENT_TO_SERVER = 0,
+    SOCKS_DIR_SERVER_TO_CLIENT = 1
+};
+
+enum SocksFlowInitiator : uint8_t
+{
+    SOCKS_INITIATOR_UNKNOWN = 0,
+    SOCKS_INITIATOR_CLIENT,
+    SOCKS_INITIATOR_SERVER
+};
+
+#pragma pack(push, 1)
+
+struct Socks5AuthNegotiation
+{
+    uint8_t version;
+    uint8_t num_methods;
+    uint8_t methods[255];
+};
+
+struct Socks5AuthResponse
+{
+    uint8_t version;
+    uint8_t method;
+};
+
+struct Socks5UsernamePasswordAuth
+{
+    uint8_t version;
+    uint8_t username_len;
+    // password_len (1 byte)
+    // password (variable length)
+};
+
+struct Socks5UsernamePasswordAuthResp
+{
+    uint8_t version; // Subnegotiation version (0x01)
+    uint8_t status;  // 0x00 = success, any other = failure
+};
+
+struct Socks5ConnectRequest
+{
+    uint8_t version;      // SOCKS version (0x05)
+    uint8_t command;      // Command (CONNECT, BIND, UDP_ASSOCIATE)
+    uint8_t reserved;     // Reserved byte (0x00)
+    uint8_t address_type; // Address type (IPv4, Domain, IPv6)
+    // address (variable length based on type)
+    // port (2 bytes)
+};
+
+// SOCKS4 structures
+struct Socks4Request
+{
+    uint8_t version;    // 0x04
+    uint8_t command;    // 0x01=CONNECT, 0x02=BIND
+    uint16_t port;      // Network byte order
+    uint32_t ip;        // Network byte order (0.0.0.x for SOCKS4a domain)
+    // userid (variable, NULL-terminated)
+    // domain (variable, NULL-terminated) - SOCKS4a only if IP is 0.0.0.x
+};
+
+struct Socks4Response
+{
+    uint8_t version;    // 0x00 (not 0x04!)
+    uint8_t status;     // 0x5A=granted, 0x5B-0x5D=rejected
+    uint16_t port;      // Network byte order
+    uint32_t ip;        // Network byte order
+};
+
+struct Socks5ConnectResponse
+{
+    uint8_t version;      // SOCKS version (0x05)
+    uint8_t reply_code;   // Reply code
+    uint8_t reserved;     // Reserved byte (0x00)
+    uint8_t address_type; // Address type
+    // address (variable length based on type)
+    // port (2 bytes)
+};
+
+struct Socks5UdpHeader
+{
+    uint16_t reserved;    // Reserved (0x0000)
+    uint8_t fragment;     // Fragment number (0x00 = no fragmentation)
+    uint8_t address_type; // Address type
+    // address (variable length based on type)
+    // port (2 bytes)
+    // data (variable length)
+};
+
+#pragma pack(pop)
+
+//-------------------------------------------------------------------------
+// Unified Address Structure - Type-safe variant pattern
+// Handles IPv4, IPv6, and domain names efficiently
+//-------------------------------------------------------------------------
+
+struct SocksAddress
+{
+    SocksAddressType type;
+    uint16_t port;
+
+    // Storage: Use string for all types, convert to SfIp when needed
+    // Rationale: Domains need string anyway, IP strings are small
+    std::string address;  // Domain name OR IP string ("10.0.0.1", "2001:db8::1")
+
+    mutable std::unique_ptr<snort::SfIp> cached_ip;
+
+    SocksAddress() : type(SOCKS_ATYP_IPV4), port(0), cached_ip(nullptr)
+    { }
+
+    bool is_set() const { return !address.empty(); }
+
+    const snort::SfIp* get_ip() const
+    {
+        if ( !cached_ip and !address.empty() )
+        {
+            auto temp_ip = std::make_unique<snort::SfIp>();
+            if ( temp_ip->set(address.c_str()) == SFIP_SUCCESS and temp_ip->is_set() )
+                cached_ip = std::move(temp_ip);
+        }
+        return cached_ip.get();
+    }
+
+    void set(const std::string& addr, SocksAddressType addr_type, uint16_t p)
+    {
+        address = addr;
+        type = addr_type;
+        port = p;
+        cached_ip.reset();
+    }
+
+    void set(const snort::SfIp& ip, uint16_t p)
+    {
+        cached_ip = std::make_unique<snort::SfIp>(ip);
+        port = p;
+
+        snort::SfIpString ip_str;
+        if ( ip.is_ip4() )
+        {
+            type = SOCKS_ATYP_IPV4;
+            address = ip.ntop(ip_str);
+        }
+        else if ( ip.is_ip6() )
+        {
+            type = SOCKS_ATYP_IPV6;
+            address = ip.ntop(ip_str);
+        }
+    }
+
+    void clear()
+    {
+        address.clear();
+        port = 0;
+        type = SOCKS_ATYP_IPV4;
+        cached_ip.reset();
+    }
+};
+
+class SocksFlowData : public snort::FlowData
+{
+public:
+    SocksFlowData();
+    ~SocksFlowData() noexcept override;
+
+    static void init();
+
+    SocksState get_state() const { return state; }
+    void set_state(SocksState new_state) { state = new_state; }
+
+    SocksDirection get_direction() const { return direction; }
+    void set_direction(SocksDirection dir) { direction = dir; }
+
+    SocksFlowInitiator get_initiator() const { return initiator; }
+    
+    void set_initiator(SocksFlowInitiator init) 
+    {
+        if (!initiator_detected())
+            initiator = init; 
+    }
+
+    bool initiator_detected() const { return initiator != SOCKS_INITIATOR_UNKNOWN; }
+
+    Socks5AuthMethod get_auth_method() const { return auth_method; }
+    void set_auth_method(Socks5AuthMethod method) { auth_method = method; }
+
+    SocksCommand get_command() const { return command; }
+    void set_command(SocksCommand cmd) { command = cmd; }
+
+    // Target destination access (unified interface)
+    const std::string& get_target_address() const { return target.address; }
+    void set_target_address(const std::string& addr) { target.address = addr; }
+
+    uint16_t get_target_port() const { return target.port; }
+    void set_target_port(uint16_t port) { target.port = port; }
+
+    SocksAddressType get_address_type() const { return target.type; }
+    void set_address_type(SocksAddressType type) { target.type = type; }
+
+    // Target IP access (binary form)
+    const snort::SfIp* get_target_ip() const { return target.get_ip(); }
+    void set_target_ip(const snort::SfIp& ip) { target.set(ip, target.port); }
+
+    const SocksAddress& get_target_address_ref() const { return target; }
+
+    // Complete target setter
+    void set_target(const std::string& addr, SocksAddressType type, uint16_t port)
+    { target.set(addr, type, port); }
+
+    void increment_request_count() { request_count++; }
+    uint32_t get_request_count() const { return request_count; }
+
+    void increment_response_count() { response_count++; }
+    uint32_t get_response_count() const { return response_count; }
+
+    // Bind address access (unified interface - consistent with target)
+    const std::string& get_bind_address() const { return bind.address; }
+    void set_bind_address(const std::string& addr) { bind.address = addr; }
+
+    uint16_t get_bind_port() const { return bind.port; }
+    void set_bind_port(uint16_t port) { bind.port = port; }
+
+    SocksAddressType get_bind_address_type() const { return bind.type; }
+    void set_bind_address_type(SocksAddressType type) { bind.type = type; }
+
+    // Bind IP access (binary form - now consistent with target)
+    const snort::SfIp* get_bind_ip() const { return bind.get_ip(); }
+    void set_bind_ip(const snort::SfIp& ip) { bind.set(ip, bind.port); }
+
+    // Complete bind setter
+    void set_bind(const std::string& addr, SocksAddressType type, uint16_t port)
+    { bind.set(addr, type, port); }
+
+    void set_last_error(SocksReplyCode error) { last_error = error; }
+    SocksReplyCode get_last_error() const { return last_error; }
+
+    bool is_handoff_pending() const { return handoff_pending; }
+    void set_handoff_pending(bool pending) { handoff_pending = pending; }
+    bool is_handoff_completed() const { return handoff_completed; }
+    void set_handoff_completed(bool completed) { handoff_completed = completed; }
+    bool is_session_counted() const { return session_counted; }
+    void set_session_counted(bool counted) { session_counted = counted; }
+
+
+
+    // SOCKS version tracking
+    uint8_t get_socks_version() const { return socks_version; }
+    void set_socks_version(uint8_t version) { socks_version = version; }
+
+    bool is_socks4a() const { return is_socks4a_protocol; }
+    void set_socks4a(bool socks4a) { is_socks4a_protocol = socks4a; }
+
+    const std::string& get_userid() const { return userid; }
+    void set_userid(const std::string& id) { userid = id; }
+
+    static unsigned get_inspector_id() { return inspector_id; }
+
+private:
+    static unsigned inspector_id;
+
+    SocksState state;
+    SocksDirection direction;
+    SocksFlowInitiator initiator;
+    uint8_t socks_version;
+    bool is_socks4a_protocol;
+
+
+    Socks5AuthMethod auth_method;
+    std::string userid;            // SOCKS4 userid OR SOCKS5 username
+
+
+    SocksCommand command;          // CONNECT, BIND, or UDP_ASSOCIATE
+
+    // Target destination (from CLIENT request)
+    // - For CONNECT: where client wants to connect
+    // - For BIND: expected incoming connection source
+    // - For UDP_ASSOCIATE: UDP relay endpoint
+    SocksAddress target;
+
+    // Bind address (from SERVER response)
+    // - For CONNECT: not used (server doesn't send bind info)
+    // - For BIND: address where server is listening
+    // - For UDP_ASSOCIATE: UDP relay address to use
+    SocksAddress bind;
+
+    //---------------------------------------------------------------------
+    // Statistics & Flow Control
+    //---------------------------------------------------------------------
+    uint32_t request_count;        // Number of SOCKS requests
+    uint32_t response_count;       // Number of SOCKS responses
+    SocksReplyCode last_error;     // Last error code from server
+    bool handoff_pending;          // Waiting to handoff to wizard
+    bool handoff_completed;        // Handoff completed
+    bool session_counted;          // Session already counted in stats
+
+};
+
+#endif
diff --git a/src/service_inspectors/socks/socks_ips.cc b/src/service_inspectors/socks/socks_ips.cc
new file mode 100644 (file)
index 0000000..244a947
--- /dev/null
@@ -0,0 +1,970 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_ips.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "socks_ips.h"
+
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "hash/hash_key_operations.h"
+#include "profiler/profiler.h"
+#include "protocols/packet.h"
+#include "socks_flow_data.h"
+
+#include <algorithm>
+#include <cctype>
+
+using namespace snort;
+
+#define s_name "socks_version"
+#define s_help "match SOCKS version (4 or 5)"
+
+//-------------------------------------------------------------------------
+// socks_version - Match SOCKS protocol version
+//-------------------------------------------------------------------------
+
+static THREAD_LOCAL ProfileStats socks_version_prof;
+
+class SocksVersionOption : public IpsOption
+{
+public:
+    SocksVersionOption(uint8_t v) : IpsOption(s_name), version(v) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    uint8_t version;
+};
+
+uint32_t SocksVersionOption::hash() const
+{
+    uint32_t a = version, b = IpsOption::hash(), c = 0;
+    mix(a, b, c);
+    finalize(a, b, c);
+    return c;
+}
+
+bool SocksVersionOption::operator==(const IpsOption& ips) const
+{
+    if ( !IpsOption::operator==(ips) )
+        return false;
+
+    const auto& rhs = static_cast<const SocksVersionOption&>(ips);
+    return version == rhs.version;
+}
+
+IpsOption::EvalStatus SocksVersionOption::eval(Cursor&, Packet* p)
+{
+    // cppcheck-suppress unreadVariable
+    RuleProfile profile(socks_version_prof);
+
+    if ( !p->flow )
+        return NO_MATCH;
+
+    const auto* fd = static_cast<const SocksFlowData*>(p->flow->get_flow_data(SocksFlowData::get_inspector_id()));
+    if ( !fd )
+        return NO_MATCH;
+
+    uint8_t flow_version = fd->get_socks_version();
+    if ( version == flow_version )
+        return MATCH;
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// socks_version module
+//-------------------------------------------------------------------------
+
+static const Parameter s_params[] =
+{
+    { "~version", Parameter::PT_INT, "4:5", nullptr,
+      "SOCKS version to match (4 or 5)" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class SocksVersionModule : public Module
+{
+public:
+    SocksVersionModule() : Module(s_name, s_help, s_params) { }
+
+    bool set(const char*, Value& v, SnortConfig*) override
+    {
+        assert(v.is("~version"));
+        version = v.get_uint8();
+        return true;
+    }
+
+    ProfileStats* get_profile() const override
+    { return &socks_version_prof; }
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    uint8_t version = SOCKS5_VERSION;
+};
+
+//-------------------------------------------------------------------------
+// socks_version api
+//-------------------------------------------------------------------------
+
+static Module* socks_version_mod_ctor()
+{ return new SocksVersionModule; }
+
+static void socks_version_mod_dtor(Module* m)
+{ delete m; }
+
+// cppcheck-suppress constParameterCallback
+static IpsOption* socks_version_ctor(Module* p, IpsInfo&)
+{
+    const auto* m = static_cast<const SocksVersionModule*>(p);
+    return new SocksVersionOption(m->version);
+}
+
+static void socks_version_dtor(IpsOption* p) 
+{ delete p; }
+
+static const IpsApi socks_version_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        socks_version_mod_ctor,
+        socks_version_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    socks_version_ctor,
+    socks_version_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// socks_state - Match SOCKS state (init/auth/request_response/established/error)
+//-------------------------------------------------------------------------
+
+#undef s_name
+#undef s_help
+#define s_name "socks_state"
+#define s_help "match SOCKS state (1=auth, 2=request_response, 3=established, 4=error)"
+
+static THREAD_LOCAL ProfileStats socks_state_prof;
+
+enum SocksStateClass : uint8_t
+{
+    SOCKS_STATE_CLASS_INIT = 0,
+    SOCKS_STATE_CLASS_AUTH,
+    SOCKS_STATE_CLASS_REQUEST_RESPONSE,
+    SOCKS_STATE_CLASS_ESTABLISHED,
+    SOCKS_STATE_CLASS_ERROR
+};
+
+static uint8_t get_socks_state_class(SocksState state)
+{
+    switch ( state )
+    {
+    case SOCKS_STATE_INIT:
+        return SOCKS_STATE_CLASS_INIT;
+
+    case SOCKS_STATE_V5_AUTH_NEGOTIATION:
+    case SOCKS_STATE_V5_AUTH_REQUEST:
+    case SOCKS_STATE_V5_AUTH_RESPONSE:
+    case SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH:
+    case SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION:
+    case SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE:
+        return SOCKS_STATE_CLASS_AUTH;
+
+    case SOCKS_STATE_V4_CONNECT_REQUEST:
+    case SOCKS_STATE_V4_CONNECT_RESPONSE:
+    case SOCKS_STATE_V4_BIND_SECOND_RESPONSE:
+    case SOCKS_STATE_V5_CONNECT_REQUEST:
+    case SOCKS_STATE_V5_CONNECT_RESPONSE:
+    case SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST:
+    case SOCKS_STATE_V5_BIND_REVERSE_CONNECT_RESPONSE:
+        return SOCKS_STATE_CLASS_REQUEST_RESPONSE;
+
+    case SOCKS_STATE_ESTABLISHED:
+        return SOCKS_STATE_CLASS_ESTABLISHED;
+
+    case SOCKS_STATE_ERROR:
+        return SOCKS_STATE_CLASS_ERROR;
+    }
+
+    return SOCKS_STATE_CLASS_ERROR;
+}
+
+static bool parse_socks_state_class(const char* s, uint8_t& out)
+{
+    if ( !s || !*s )
+        return false;
+
+    bool ok = false;
+    uint64_t value = Parameter::get_uint(s, ok);
+    if ( ok )
+    {
+        if ( value >= SOCKS_STATE_CLASS_AUTH && value <= SOCKS_STATE_CLASS_ERROR )
+        {
+            out = static_cast<uint8_t>(value);
+            return true;
+        }
+        return false;
+    }
+
+    std::string key(s);
+    std::transform(key.begin(), key.end(), key.begin(),
+        [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
+    std::replace(key.begin(), key.end(), '-', '_');
+
+    if ( key == "auth" )
+        out = SOCKS_STATE_CLASS_AUTH;
+    else if ( key == "request_response" )
+        out = SOCKS_STATE_CLASS_REQUEST_RESPONSE;
+    else if ( key == "established" )
+        out = SOCKS_STATE_CLASS_ESTABLISHED;
+    else if ( key == "error" )
+        out = SOCKS_STATE_CLASS_ERROR;
+    else
+        return false;
+
+    return true;
+}
+
+class SocksStateOption : public IpsOption
+{
+public:
+    SocksStateOption(uint8_t s) : IpsOption(s_name), state_class(s) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    uint8_t state_class;
+};
+
+uint32_t SocksStateOption::hash() const
+{
+    uint32_t a = state_class, b = IpsOption::hash(), c = 0;
+    mix(a, b, c);
+    finalize(a, b, c);
+    return c;
+}
+
+bool SocksStateOption::operator==(const IpsOption& ips) const
+{
+    if ( !IpsOption::operator==(ips) )
+        return false;
+
+    const auto& rhs = static_cast<const SocksStateOption&>(ips);
+    return state_class == rhs.state_class;
+}
+
+IpsOption::EvalStatus SocksStateOption::eval(Cursor&, Packet* p)
+{
+    // cppcheck-suppress unreadVariable
+    RuleProfile profile(socks_state_prof);
+
+    if ( !p->flow )
+        return NO_MATCH;
+
+    const auto* fd = static_cast<const SocksFlowData*>(p->flow->get_flow_data(SocksFlowData::get_inspector_id()));
+    if ( !fd )
+        return NO_MATCH;
+
+    uint8_t flow_class = get_socks_state_class(fd->get_state());
+    if ( flow_class == state_class )
+        return MATCH;
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// socks_state module
+//-------------------------------------------------------------------------
+
+static const Parameter socks_state_params[] =
+{
+    { "~state", Parameter::PT_STRING, nullptr, nullptr,
+      "state to match (1-4 or auth|request_response|established|error)" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class SocksStateModule : public Module
+{
+public:
+    SocksStateModule() : Module(s_name, s_help, socks_state_params) { }
+
+    bool set(const char*, Value& v, SnortConfig*) override
+    {
+        assert(v.is("~state"));
+        uint8_t parsed = 0;
+        if ( !parse_socks_state_class(v.get_string(), parsed) )
+            return false;
+        state_class = parsed;
+        return true;
+    }
+
+    ProfileStats* get_profile() const override
+    { return &socks_state_prof; }
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    uint8_t state_class = SOCKS_STATE_CLASS_ESTABLISHED;
+};
+
+//-------------------------------------------------------------------------
+// socks_state api
+//-------------------------------------------------------------------------
+
+static Module* socks_state_mod_ctor()
+{ return new SocksStateModule; }
+
+static void socks_state_mod_dtor(Module* m)
+{ delete m; }
+
+// cppcheck-suppress constParameterCallback
+static IpsOption* socks_state_ctor(Module* p, IpsInfo&)
+{
+    const auto* m = static_cast<const SocksStateModule*>(p);
+    return new SocksStateOption(m->state_class);
+}
+
+static void socks_state_dtor(IpsOption* p)
+{ delete p; }
+
+static const IpsApi socks_state_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        socks_state_mod_ctor,
+        socks_state_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    socks_state_ctor,
+    socks_state_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// socks_command - Match SOCKS command
+//-------------------------------------------------------------------------
+
+#undef s_name
+#undef s_help
+#define s_name "socks_command"
+#define s_help "match SOCKS command (1=CONNECT, 2=BIND, 3=UDP_ASSOCIATE)"
+
+static THREAD_LOCAL ProfileStats socks_command_prof;
+
+class SocksCommandOption : public IpsOption
+{
+public:
+    SocksCommandOption(uint8_t c) : IpsOption(s_name), command(c) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    uint8_t command;
+};
+
+uint32_t SocksCommandOption::hash() const
+{
+    uint32_t a = command, b = IpsOption::hash(), c = 0;
+    mix(a, b, c);
+    finalize(a, b, c);
+    return c;
+}
+
+bool SocksCommandOption::operator==(const IpsOption& ips) const
+{
+    if ( !IpsOption::operator==(ips) )
+        return false;
+
+    const auto& rhs = static_cast<const SocksCommandOption&>(ips);
+    return command == rhs.command;
+}
+
+IpsOption::EvalStatus SocksCommandOption::eval(Cursor&, Packet* p)
+{
+    // cppcheck-suppress unreadVariable
+    RuleProfile profile(socks_command_prof);
+
+    if ( !p->flow )
+        return NO_MATCH;
+
+    const auto* fd = static_cast<const SocksFlowData*>(p->flow->get_flow_data(SocksFlowData::get_inspector_id()));
+    if ( !fd )
+        return NO_MATCH;
+
+    // Only evaluate if target address has been set (SOCKS request parsed)
+    if ( fd->get_target_address().empty() )
+        return NO_MATCH;
+
+    if ( fd->get_command() == static_cast<SocksCommand>(command) )
+        return MATCH;
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// socks_command module
+//-------------------------------------------------------------------------
+
+static const Parameter socks_command_params[] =
+{
+    { "~command", Parameter::PT_INT, "1:3", nullptr,
+      "SOCKS command (1=CONNECT, 2=BIND, 3=UDP_ASSOCIATE)" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class SocksCommandModule : public Module
+{
+public:
+    SocksCommandModule() : Module(s_name, s_help, socks_command_params) { }
+
+    bool set(const char*, Value& v, SnortConfig*) override
+    {
+        assert(v.is("~command"));
+        command = v.get_uint8();
+        return true;
+    }
+
+    ProfileStats* get_profile() const override
+    { return &socks_command_prof; }
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    uint8_t command = SOCKS_CMD_CONNECT;
+};
+
+//-------------------------------------------------------------------------
+// socks_command api
+//-------------------------------------------------------------------------
+
+static Module* socks_command_mod_ctor() 
+{ return new SocksCommandModule; }
+
+static void socks_command_mod_dtor(Module* m) 
+{ delete m; }
+
+// cppcheck-suppress constParameterCallback
+static IpsOption* socks_command_ctor(Module* p, IpsInfo&)
+{
+    const auto* m = static_cast<const SocksCommandModule*>(p);
+    return new SocksCommandOption(m->command);
+}
+
+static void socks_command_dtor(IpsOption* p)
+{ delete p; }
+
+static const IpsApi socks_command_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        socks_command_mod_ctor,
+        socks_command_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    socks_command_ctor,
+    socks_command_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// socks_address_type - Match SOCKS address type (SOCKS5-specific)
+//-------------------------------------------------------------------------
+
+#undef s_name
+#undef s_help
+#define s_name "socks_address_type"
+#define s_help "match SOCKS address type (1=IPv4, 3=Domain, 4=IPv6) - SOCKS5 only"
+
+static THREAD_LOCAL ProfileStats socks5_address_type_prof;
+
+class Socks5AddressTypeOption : public IpsOption
+{
+public:
+    Socks5AddressTypeOption(uint8_t t) : IpsOption(s_name), addr_type(t) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    uint8_t addr_type;
+};
+
+uint32_t Socks5AddressTypeOption::hash() const
+{
+    uint32_t a = addr_type, b = IpsOption::hash(), c = 0;
+    mix(a, b, c);
+    finalize(a, b, c);
+    return c;
+}
+
+bool Socks5AddressTypeOption::operator==(const IpsOption& ips) const
+{
+    if ( !IpsOption::operator==(ips) )
+        return false;
+
+    const auto& rhs = static_cast<const Socks5AddressTypeOption&>(ips);
+    return addr_type == rhs.addr_type;
+}
+
+IpsOption::EvalStatus Socks5AddressTypeOption::eval(Cursor&, Packet* p)
+{
+    // cppcheck-suppress unreadVariable
+    RuleProfile profile(socks5_address_type_prof);
+
+    if ( !p->flow )
+        return NO_MATCH;
+
+    const auto* fd = static_cast<const SocksFlowData*>(p->flow->get_flow_data(SocksFlowData::get_inspector_id()));
+    if ( !fd )
+        return NO_MATCH;
+
+    // Only evaluate if target address has been set (SOCKS request parsed)
+    if ( fd->get_target_address().empty() )
+        return NO_MATCH;
+
+    if ( fd->get_address_type() == static_cast<SocksAddressType>(addr_type) )
+        return MATCH;
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// socks5_address_type module
+//-------------------------------------------------------------------------
+
+static const Parameter socks5_address_type_params[] =
+{
+    { "~type", Parameter::PT_INT, "1:4", nullptr,
+      "address type (1=IPv4, 3=Domain, 4=IPv6)" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class Socks5AddressTypeModule : public Module
+{
+public:
+    Socks5AddressTypeModule() : Module(s_name, s_help, socks5_address_type_params) { }
+
+    bool set(const char*, Value& v, SnortConfig*) override
+    {
+        assert(v.is("~type"));
+        addr_type = v.get_uint8();
+        return true;
+    }
+
+    ProfileStats* get_profile() const override
+    { return &socks5_address_type_prof; }
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    uint8_t addr_type = SOCKS_ATYP_IPV4;
+};
+
+//-------------------------------------------------------------------------
+// socks5_address_type api
+//-------------------------------------------------------------------------
+
+static Module* socks5_address_type_mod_ctor()
+{ return new Socks5AddressTypeModule; }
+
+static void socks5_address_type_mod_dtor(Module* m)
+{ delete m; }
+
+// cppcheck-suppress constParameterCallback
+static IpsOption* socks5_address_type_ctor(Module* p, IpsInfo&)
+{
+    const auto* m = static_cast<const Socks5AddressTypeModule*>(p);
+    return new Socks5AddressTypeOption(m->addr_type);
+}
+
+static void socks5_address_type_dtor(IpsOption* p) 
+{ delete p; }
+
+static const IpsApi socks5_address_type_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        socks5_address_type_mod_ctor,
+        socks5_address_type_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    socks5_address_type_ctor,
+    socks5_address_type_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// socks_remote_address - Buffer option for destination address
+//-------------------------------------------------------------------------
+
+#undef s_name
+#undef s_help
+#define s_name "socks_remote_address"
+#define s_help "set cursor to remote destination address (IP or domain)"
+
+static THREAD_LOCAL ProfileStats socks_remote_address_prof;
+
+class SocksRemoteAddressOption : public IpsOption
+{
+public:
+    SocksRemoteAddressOption(const std::string& addr = "") : IpsOption(s_name), match_addr(addr) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+    CursorActionType get_cursor_type() const override
+    { return match_addr.empty() ? CAT_SET_FAST_PATTERN : CAT_NONE; }
+
+private:
+    std::string match_addr;
+};
+
+uint32_t SocksRemoteAddressOption::hash() const
+{
+    uint32_t a = IpsOption::hash(), b = 0, c = 0;
+    mix_str(a, b, c, match_addr.c_str());
+    finalize(a, b, c);
+    return c;
+}
+
+bool SocksRemoteAddressOption::operator==(const IpsOption& ips) const
+{
+    if ( !IpsOption::operator==(ips) )
+        return false;
+    const SocksRemoteAddressOption& rhs = static_cast<const SocksRemoteAddressOption&>(ips);
+    return match_addr == rhs.match_addr;
+}
+
+IpsOption::EvalStatus SocksRemoteAddressOption::eval(Cursor& c, Packet* p)
+{
+    // cppcheck-suppress unreadVariable
+    RuleProfile profile(socks_remote_address_prof);
+
+    if ( !p->flow )
+        return NO_MATCH;
+
+    const auto* fd = static_cast<const SocksFlowData*>(p->flow->get_flow_data(SocksFlowData::get_inspector_id()));
+    if ( !fd )
+        return NO_MATCH;
+
+    const std::string& addr = fd->get_target_address();
+    if ( addr.empty() )
+        return NO_MATCH;
+
+    // If match_addr is set, do direct string comparison
+    if ( !match_addr.empty() )
+        return (addr.find(match_addr) != std::string::npos) ? MATCH : NO_MATCH;
+
+    // Otherwise, set cursor for content matching
+    c.set(s_name, reinterpret_cast<const uint8_t*>(addr.data()), addr.length());
+    return MATCH;
+}
+
+//-------------------------------------------------------------------------
+// socks_remote_address module
+//-------------------------------------------------------------------------
+
+static const Parameter socks_remote_address_params[] =
+{
+    { "~", Parameter::PT_STRING, nullptr, nullptr, "address to match (substring)" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class SocksRemoteAddressModule : public Module
+{
+public:
+    SocksRemoteAddressModule() : Module(s_name, s_help, socks_remote_address_params) { }
+
+    bool set(const char*, Value& v, SnortConfig*) override
+    {
+        if ( v.is("~") )
+            addr = v.get_string();
+        return true;
+    }
+
+    ProfileStats* get_profile() const override
+    { return &socks_remote_address_prof; }
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+    std::string addr;
+};
+
+//-------------------------------------------------------------------------
+// socks_remote_address api
+//-------------------------------------------------------------------------
+
+static Module* socks_remote_address_mod_ctor() 
+{ return new SocksRemoteAddressModule; }
+
+static void socks_remote_address_mod_dtor(Module* m) 
+{ delete m; }
+
+// cppcheck-suppress constParameterCallback ; signature must match Module callback type
+static IpsOption* socks_remote_address_ctor(Module* m, IpsInfo&)
+{
+    const SocksRemoteAddressModule* mod = static_cast<const SocksRemoteAddressModule*>(m);
+    return new SocksRemoteAddressOption(mod->addr);
+}
+
+static void socks_remote_address_dtor(IpsOption* p) 
+{ delete p; }
+
+static const IpsApi socks_remote_address_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        socks_remote_address_mod_ctor,
+        socks_remote_address_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    socks_remote_address_ctor,
+    socks_remote_address_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// socks_remote_port - Match destination port
+//-------------------------------------------------------------------------
+
+#undef s_name
+#undef s_help
+#define s_name "socks_remote_port"
+#define s_help "match SOCKS remote destination port"
+
+static THREAD_LOCAL ProfileStats socks_remote_port_prof;
+
+class SocksRemotePortOption : public IpsOption
+{
+public:
+    SocksRemotePortOption(uint16_t p) : IpsOption(s_name), port(p) { }
+
+    uint32_t hash() const override;
+    bool operator==(const IpsOption&) const override;
+    EvalStatus eval(Cursor&, Packet*) override;
+
+private:
+    uint16_t port;
+};
+
+uint32_t SocksRemotePortOption::hash() const
+{
+    uint32_t a = port, b = IpsOption::hash(), c = 0;
+    mix(a, b, c);
+    finalize(a, b, c);
+    return c;
+}
+
+bool SocksRemotePortOption::operator==(const IpsOption& ips) const
+{
+    if ( !IpsOption::operator==(ips) )
+        return false;
+
+    const auto& rhs = static_cast<const SocksRemotePortOption&>(ips);
+    return port == rhs.port;
+}
+
+IpsOption::EvalStatus SocksRemotePortOption::eval(Cursor&, Packet* p)
+{
+    // cppcheck-suppress unreadVariable
+    RuleProfile profile(socks_remote_port_prof);
+
+    if ( !p->flow )
+        return NO_MATCH;
+
+    const auto* fd = static_cast<const SocksFlowData*>(p->flow->get_flow_data(SocksFlowData::get_inspector_id()));
+    if ( !fd )
+        return NO_MATCH;
+
+    // Only evaluate if target port has been set (SOCKS request parsed)
+    // Port 0 is not a valid target port
+    if ( fd->get_target_port() == 0 )
+        return NO_MATCH;
+
+    if ( fd->get_target_port() == port )
+        return MATCH;
+
+    return NO_MATCH;
+}
+
+//-------------------------------------------------------------------------
+// socks_remote_port module
+//-------------------------------------------------------------------------
+
+static const Parameter socks_remote_port_params[] =
+{
+    { "~port", Parameter::PT_PORT, nullptr, nullptr,
+      "destination port to match" },
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+class SocksRemotePortModule : public Module
+{
+public:
+    SocksRemotePortModule() : Module(s_name, s_help, socks_remote_port_params) { }
+
+    bool set(const char*, Value& v, SnortConfig*) override
+    {
+        assert(v.is("~port"));
+        port = v.get_uint16();
+        return true;
+    }
+
+    ProfileStats* get_profile() const override
+    { return &socks_remote_port_prof; }
+
+    Usage get_usage() const override
+    { return DETECT; }
+
+public:
+    uint16_t port = 0;
+};
+
+//-------------------------------------------------------------------------
+// socks_remote_port api
+//-------------------------------------------------------------------------
+
+static Module* socks_remote_port_mod_ctor() 
+{ return new SocksRemotePortModule; }
+
+static void socks_remote_port_mod_dtor(Module* m) 
+{ delete m; }
+
+// cppcheck-suppress constParameterCallback
+static IpsOption* socks_remote_port_ctor(Module* p, IpsInfo&)
+{
+    const auto* m = static_cast<const SocksRemotePortModule*>(p);
+    return new SocksRemotePortOption(m->port);
+}
+
+static void socks_remote_port_dtor(IpsOption* p)
+{ delete p; }
+
+static const IpsApi socks_remote_port_api =
+{
+    {
+        PT_IPS_OPTION,
+        sizeof(IpsApi),
+        IPSAPI_VERSION,
+        0,
+        API_RESERVED,
+        API_OPTIONS,
+        s_name,
+        s_help,
+        socks_remote_port_mod_ctor,
+        socks_remote_port_mod_dtor
+    },
+    OPT_TYPE_DETECTION,
+    0, PROTO_BIT__TCP | PROTO_BIT__UDP,
+    nullptr,
+    nullptr,
+    nullptr,
+    nullptr,
+    socks_remote_port_ctor,
+    socks_remote_port_dtor,
+    nullptr
+};
+
+//-------------------------------------------------------------------------
+// plugin exports
+//-------------------------------------------------------------------------
+
+const BaseApi* ips_socks_version = &socks_version_api.base;
+const BaseApi* ips_socks_state = &socks_state_api.base;
+const BaseApi* ips_socks_command = &socks_command_api.base;
+const BaseApi* ips_socks_address_type = &socks5_address_type_api.base;
+const BaseApi* ips_socks_remote_address = &socks_remote_address_api.base;
+const BaseApi* ips_socks_remote_port = &socks_remote_port_api.base;
diff --git a/src/service_inspectors/socks/socks_ips.h b/src/service_inspectors/socks/socks_ips.h
new file mode 100644 (file)
index 0000000..421d814
--- /dev/null
@@ -0,0 +1,34 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_ips.h author Raza Shafiq <rshafiq@cisco.com>
+
+#ifndef SOCKS_IPS_H
+#define SOCKS_IPS_H
+
+namespace snort
+{
+    struct SnortConfig;
+}
+
+void ips_socks_version_init();
+void ips_socks_command_init();
+void ips_socks_address_type_init();
+void ips_socks_remote_address_init();
+void ips_socks_remote_port_init();
+
+#endif
diff --git a/src/service_inspectors/socks/socks_module.cc b/src/service_inspectors/socks/socks_module.cc
new file mode 100644 (file)
index 0000000..478e57e
--- /dev/null
@@ -0,0 +1,117 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_module.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "socks_module.h"
+
+#include "framework/decode_data.h"
+#include "socks.h"
+
+using namespace snort;
+
+// Performance counters
+const PegInfo socks_pegs[] =
+{
+    { CountType::SUM, "sessions", "total SOCKS sessions" },
+    { CountType::NOW, "concurrent_sessions", "current concurrent SOCKS sessions" },
+    { CountType::MAX, "max_concurrent_sessions", "maximum concurrent SOCKS sessions" },
+    { CountType::SUM, "auth_requests", "authentication requests" },
+    { CountType::SUM, "auth_successes", "successful authentications" },
+    { CountType::SUM, "auth_failures", "failed authentications" },
+    { CountType::SUM, "connect_requests", "CONNECT requests" },
+    { CountType::SUM, "bind_requests", "BIND requests" },
+    { CountType::SUM, "udp_associate_requests", "UDP ASSOCIATE requests" },
+    { CountType::SUM, "successful_connections", "successful connections" },
+    { CountType::SUM, "failed_connections", "failed connections" },
+    { CountType::SUM, "udp_associations_created", "UDP ASSOCIATE completions" },
+    { CountType::SUM, "udp_expectations_created", "UDP expectations created for dynamic ports" },
+    { CountType::SUM, "udp_packets", "UDP packets processed" },
+    { CountType::SUM, "udp_frags_dropped", "UDP fragments dropped" },
+    { CountType::SUM, "udp_frags_blocked", "flows blocked due to UDP fragmentation" },
+    { CountType::END, nullptr, nullptr }
+};
+
+static const Parameter socks_params[] =
+{
+    { "block_udp_fragmentation", Parameter::PT_BOOL, nullptr, "true",
+      "block flow when SOCKS5 UDP fragmentation detected (frag > 0)" },
+
+    { nullptr, Parameter::PT_MAX, nullptr, nullptr, nullptr }
+};
+
+static const RuleMap socks_rules[] =
+{
+    // SOCKS protocol anomaly events (security-relevant only)
+    { SOCKS_EVENT_UNKNOWN_COMMAND, "SOCKS unknown command" },
+    { SOCKS_EVENT_PROTOCOL_VIOLATION, "SOCKS protocol violation" },
+    
+    // SOCKS5-specific events
+    { SOCKS5_EVENT_UNKNOWN_ADDRESS_TYPE, "SOCKS5 unknown address type" },
+    { SOCKS5_EVENT_UDP_FRAGMENTATION, "SOCKS5 UDP fragmentation detected" },
+    
+    { 0, nullptr }
+};
+
+SocksModule::SocksModule() : Module(SOCKS_NAME, SOCKS_HELP, socks_params)
+{
+    config = nullptr;
+}
+
+SocksModule::~SocksModule()
+{
+    if ( config )
+        delete config;
+}
+
+bool SocksModule::set(const char*, Value& v, SnortConfig*)
+{
+    assert(config);
+
+    if ( v.is("block_udp_fragmentation") )
+        config->block_udp_fragmentation = v.get_bool();
+    else
+        return false;
+
+    return true;
+}
+
+bool SocksModule::begin(const char*, int, SnortConfig*)
+{
+    if ( !config )
+        config = new SocksConfig();
+    return true;
+}
+
+bool SocksModule::end(const char*, int, SnortConfig*)
+{
+    return true;
+}
+
+const RuleMap* SocksModule::get_rules() const
+{ return socks_rules; }
+
+const PegInfo* SocksModule::get_pegs() const
+{ return socks_pegs; }
+
+PegCount* SocksModule::get_counts() const
+{ return const_cast<PegCount*>(reinterpret_cast<const PegCount*>(&socks_stats)); }
diff --git a/src/service_inspectors/socks/socks_module.h b/src/service_inspectors/socks/socks_module.h
new file mode 100644 (file)
index 0000000..03e0993
--- /dev/null
@@ -0,0 +1,130 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_module.h author Raza Shafiq <rshafiq@cisco.com>
+
+#ifndef SOCKS_MODULE_H
+#define SOCKS_MODULE_H
+
+#include "framework/module.h"
+#include "framework/decode_data.h"
+#include "profiler/profiler.h"
+#include "trace/trace_api.h"
+
+#define SOCKS_NAME "socks"
+#define SOCKS_HELP "SOCKS protocol inspector"
+
+#define GID_SOCKS 155
+
+enum SocksEvent : uint32_t
+{
+    // SOCKS protocol anomaly events (security-relevant only)
+    SOCKS_EVENT_UNKNOWN_COMMAND = 1,
+    SOCKS_EVENT_PROTOCOL_VIOLATION,
+    
+    // SOCKS5-specific events
+    SOCKS5_EVENT_UNKNOWN_ADDRESS_TYPE,
+    SOCKS5_EVENT_UDP_FRAGMENTATION
+};
+
+enum SocksPeg : uint32_t
+{
+    SOCKS_PEG_SESSIONS = 0,
+    SOCKS_PEG_CONCURRENT_SESSIONS,
+    SOCKS_PEG_MAX_CONCURRENT_SESSIONS,
+    SOCKS_PEG_AUTH_REQUESTS,
+    SOCKS_PEG_AUTH_SUCCESSES,
+    SOCKS_PEG_AUTH_FAILURES,
+    SOCKS_PEG_CONNECT_REQUESTS,
+    SOCKS_PEG_BIND_REQUESTS,
+    SOCKS_PEG_UDP_ASSOCIATE_REQUESTS,
+    SOCKS_PEG_SUCCESSFUL_CONNECTIONS,
+    SOCKS_PEG_FAILED_CONNECTIONS,
+    SOCKS_PEG_UDP_ASSOCIATIONS_CREATED,
+    SOCKS_PEG_UDP_EXPECTATIONS_CREATED,
+    SOCKS_PEG_UDP_PACKETS,
+    SOCKS_PEG_UDP_FRAGS_DROPPED,
+    SOCKS_PEG_UDP_FRAGS_BLOCKED,
+    SOCKS_PEG_MAX
+};
+
+extern const PegInfo socks_pegs[];
+
+struct SocksStats
+{
+    PegCount sessions;
+    PegCount concurrent_sessions;
+    PegCount max_concurrent_sessions;
+    PegCount auth_requests;
+    PegCount auth_successes;
+    PegCount auth_failures;
+    PegCount connect_requests;
+    PegCount bind_requests;
+    PegCount udp_associate_requests;
+    PegCount successful_connections;
+    PegCount failed_connections;
+    PegCount udp_associations_created;
+    PegCount udp_expectations_created;
+    PegCount udp_packets;
+    PegCount udp_frags_dropped;
+    PegCount udp_frags_blocked;
+};
+
+extern THREAD_LOCAL SocksStats socks_stats;
+extern THREAD_LOCAL snort::ProfileStats socksPerfStats;
+
+struct SocksConfig
+{
+    bool block_udp_fragmentation = true;     // Block flow on UDP fragmentation (default: true)
+};
+
+class SocksModule : public snort::Module
+{
+public:
+    SocksModule();
+    ~SocksModule() override;
+
+    unsigned get_gid() const override
+    { return GID_SOCKS; }
+
+    const snort::RuleMap* get_rules() const override;
+
+    bool set(const char*, snort::Value&, snort::SnortConfig*) override;
+    bool begin(const char*, int, snort::SnortConfig*) override;
+    bool end(const char*, int, snort::SnortConfig*) override;
+
+    const PegInfo* get_pegs() const override;
+    PegCount* get_counts() const override;
+
+    snort::ProfileStats* get_profile() const override
+    { return &socksPerfStats; }
+
+    Usage get_usage() const override
+    { return INSPECT; }
+
+    bool is_bindable() const override
+    { return true; }
+    
+    const SocksConfig* get_config() const
+    { return config; }
+
+private:
+    SocksConfig* config;
+};
+
+#endif
diff --git a/src/service_inspectors/socks/socks_splitter.cc b/src/service_inspectors/socks/socks_splitter.cc
new file mode 100644 (file)
index 0000000..7243a3b
--- /dev/null
@@ -0,0 +1,399 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_splitter.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "socks_splitter.h"
+
+#include <iomanip>
+#include <sstream>
+
+#include "flow/flow.h"
+#include "protocols/packet.h"
+#include "socks_flow_data.h"
+
+using namespace snort;
+
+
+SocksSplitter::SocksSplitter(bool c2s) : StreamSplitter(c2s)
+{ }
+
+StreamSplitter::Status SocksSplitter::scan(
+    Packet* p, const uint8_t* data, uint32_t len,
+    uint32_t flags, uint32_t* fp)
+{
+    if ( !data or len == 0 )
+        return SEARCH;
+
+    const Flow* flow = p->flow;
+    const SocksFlowData* flow_data = static_cast<const SocksFlowData*>(
+        flow->get_flow_data(SocksFlowData::get_inspector_id()));
+
+    // After tunnel is established or handoff completed, STOP scanning
+    // to allow wizard to take over for tunneled protocol detection
+    if ( flow_data and (flow_data->get_state() == SOCKS_STATE_ESTABLISHED or
+                        flow_data->is_handoff_completed()) )
+    {
+        return STOP;
+    }
+
+    bool from_client = (flags & PKT_FROM_CLIENT) != 0;
+    uint32_t msg_len = 0;
+    SocksState state = flow_data ? flow_data->get_state() : SOCKS_STATE_INIT;
+
+    // ERROR state: flush data to allow error handling at higher layers
+    if ( state == SOCKS_STATE_ERROR )
+    {
+        *fp = len;
+        return FLUSH;
+    }
+
+    if ( from_client )
+        msg_len = parse_client_packet(data, len, state);
+    else
+        msg_len = parse_server_packet(data, len, state);
+
+    if ( msg_len > 0 and msg_len <= len )
+    {
+        *fp = msg_len;
+        return FLUSH;
+    }
+
+    return SEARCH;
+}
+
+uint32_t SocksSplitter::parse_client_packet(const uint8_t* data, uint32_t len, SocksState state)
+{
+    switch (state)
+    {
+        case SOCKS_STATE_INIT:
+        case SOCKS_STATE_V5_AUTH_NEGOTIATION:
+            if ( len >= 9 and data[0] == SOCKS4_VERSION )
+            {
+                uint32_t socks4_len = parse_socks4_request(data, len);
+                if ( socks4_len > 0 )
+                    return socks4_len;
+            }
+
+            if ( len >= 2 and data[0] == SOCKS5_VERSION )
+            {
+                uint32_t auth_len = parse_auth_negotiation(data, len);
+                if ( auth_len > 0 )
+                    return auth_len;
+            }
+            break;
+
+        case SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH:
+            return parse_username_password_auth(data, len);
+
+        case SOCKS_STATE_V5_CONNECT_REQUEST:
+            return parse_connect_request(data, len);
+
+        case SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE:
+            if ( len >= 2 && data[0] == SOCKS5_USERPASS_VERSION )
+                return parse_username_password_auth(data, len);
+
+            return parse_auth_negotiation(data, len);
+
+        case SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION:
+        {
+            // Client sends auth method selection (2 bytes) or may pipeline username/password auth
+            uint32_t auth_len = parse_auth_response(data, len);
+            if ( auth_len )
+                return auth_len;
+
+            return parse_username_password_auth(data, len);
+        }
+
+        case SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST:
+        {
+            // Server may pipeline auth negotiation + connect request before client responds.
+            // Client still sends auth response first (exactly 2 bytes), then connect response.
+            // Check for combined auth+connect: 05 XX 05 YY 00 ... (auth response followed by connect response)
+            if ( len >= 4 and data[0] == SOCKS5_VERSION and data[2] == SOCKS5_VERSION )
+            {
+                // Combined PDU detected - flush auth response (2 bytes), inspector will rescan
+                return 2;
+            }
+
+            // Standalone auth response (exactly 2 bytes)
+            if ( len == 2 )
+            {
+                uint32_t auth_len = parse_auth_response(data, len);
+                if ( auth_len )
+                    return auth_len;
+            }
+
+            return parse_connect_response(data, len);
+        }
+
+        case SOCKS_STATE_V5_BIND_REVERSE_CONNECT_RESPONSE:
+            return parse_connect_request(data, len);
+
+        // ESTABLISHED and ERROR states are now handled at the top of scan()
+        // by returning STOP, so we should never reach here for those states
+        case SOCKS_STATE_ESTABLISHED:
+        case SOCKS_STATE_ERROR:
+            return 0;  // Should not reach here
+
+        default:
+            break;
+    }
+
+    return 0;
+}
+
+uint32_t SocksSplitter::parse_server_packet(const uint8_t* data, uint32_t len, SocksState state)
+{
+    switch (state)
+    {
+        case SOCKS_STATE_INIT:
+        case SOCKS_STATE_V5_AUTH_NEGOTIATION:
+            return parse_auth_response(data, len);
+
+        case SOCKS_STATE_V4_CONNECT_RESPONSE:
+            return parse_socks4_response(data, len);
+
+        case SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH:
+            return parse_username_password_auth_response(data, len);
+
+        case SOCKS_STATE_V5_CONNECT_REQUEST:
+            return parse_connect_response(data, len);
+
+        case SOCKS_STATE_V5_CONNECT_RESPONSE:
+            return parse_connect_response(data, len);
+
+        case SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION:
+            return parse_auth_response(data, len);
+
+        case SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE:
+            return parse_connect_response(data, len);
+
+        case SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST:
+            return parse_connect_response(data, len);
+
+        // Terminal states: return full length to flush all remaining data
+        case SOCKS_STATE_V5_BIND_REVERSE_CONNECT_RESPONSE:
+        case SOCKS_STATE_ESTABLISHED:
+        case SOCKS_STATE_ERROR:
+            return len;
+
+        default:
+            break;
+    }
+    
+    return 0;
+}
+
+uint32_t SocksSplitter::parse_auth_negotiation(const uint8_t* data, uint32_t len)
+{
+    // Format: VER(1) + NMETHODS(1) + METHODS(1-255)
+    if ( len < 2 or data[0] != 0x05 )
+        return 0;
+
+    uint8_t num_methods = data[1];
+
+    if ( num_methods == 0 )
+        return 0;
+    
+    uint32_t expected_len = 2 + num_methods;
+
+    if ( len >= expected_len )
+        return expected_len;
+    
+    return 0;
+}
+
+uint32_t SocksSplitter::parse_auth_response(const uint8_t* data, uint32_t len)
+{
+    if ( len < 2 )
+        return 0;
+
+    // SOCKS5 auth response: 05 XX
+    if ( data[0] == 0x05 )
+    {
+        uint8_t auth_method = data[1];
+
+        if ( auth_method != 0x00 and auth_method != 0x02 and auth_method != 0xFF )
+            return len;  // Consume entire packet for unsupported auth
+        
+        return 2;
+    }
+
+    // SOCKS4 response: 00 XX ... (8 bytes total)
+    if ( data[0] == 0x00 )
+    {
+        return parse_socks4_response(data, len);
+    }
+
+    return 0;
+}
+
+uint32_t SocksSplitter::parse_username_password_auth(const uint8_t* data, uint32_t len)
+{
+    // Format: VER(1) + ULEN(1) + UNAME(1-255) + PLEN(1) + PASSWD(1-255)
+    if ( len < 3 or data[0] != SOCKS5_USERPASS_VERSION )
+        return 0;
+
+    uint8_t ulen = data[1];
+    if ( len < 3 + static_cast<uint32_t>(ulen) )
+        return 0;
+
+    uint8_t plen = data[2 + ulen];
+    uint32_t required_len = 3 + ulen + plen;
+
+    if ( len >= required_len )
+        return required_len;
+    
+    return 0;
+}
+
+uint32_t SocksSplitter::parse_username_password_auth_response(const uint8_t* data, uint32_t len)
+{
+    // Format: VER(1) + STATUS(1)
+    if ( len < 2 or data[0] != SOCKS5_USERPASS_VERSION )
+        return 0;
+
+    return 2;
+}
+
+uint32_t SocksSplitter::parse_connect_request(const uint8_t* data, uint32_t len)
+{
+    // Format: VER(1) + CMD(1) + RSV(1) + ATYP(1) + DST.ADDR(var) + DST.PORT(2)
+    if ( len < 4 or data[0] != 0x05 )
+        return 0;
+
+    return parse_address_port_length(data, len, 3);
+}
+
+uint32_t SocksSplitter::parse_connect_response(const uint8_t* data, uint32_t len)
+{
+    // Format: VER(1) + REP(1) + RSV(1) + ATYP(1) + BND.ADDR(var) + BND.PORT(2)
+    if ( len < 4 or data[0] != 0x05 )
+        return 0;
+
+    return parse_address_port_length(data, len, 3);
+}
+
+uint32_t SocksSplitter::parse_address_port_length(const uint8_t* data, uint32_t len, uint32_t atyp_offset)
+{
+    if ( len <= atyp_offset )
+        return 0;
+
+    uint8_t atyp = data[atyp_offset];
+    uint32_t addr_len;
+
+    switch (atyp)
+    {
+        case 0x01: // IPv4
+            addr_len = 4;
+            break;
+        case 0x03: // Domain name
+        {
+            if ( len < atyp_offset + 2 )
+                return 0;
+            // Validate domain length to prevent integer overflow and DoS
+            uint8_t domain_len = data[atyp_offset + 1];
+            if ( domain_len == 0 or domain_len > 253 )  // RFC 1035 max domain length
+                return 0;
+            addr_len = 1 + static_cast<uint32_t>(domain_len);  // Explicit cast to prevent overflow
+            break;
+        }
+        case 0x04: // IPv6
+            addr_len = 16;
+            break;
+        default:
+            return 0;
+    }
+
+    uint32_t required_len = atyp_offset + 1 + addr_len + 2;
+
+    if ( len >= required_len )
+        return required_len;
+    
+    return 0;
+}
+
+//-------------------------------------------------------------------------
+// SOCKS4 parsing functions
+//-------------------------------------------------------------------------
+
+uint32_t SocksSplitter::parse_socks4_request(const uint8_t* data, uint32_t len)
+{
+    // SOCKS4 request: VER(1) CMD(1) PORT(2) IP(4) USERID(variable) NULL(1)
+    // Minimum: 9 bytes
+    if ( len < 9 or data[0] != 0x04 )
+        return 0;
+
+    // Find NULL terminator for userid (starts at offset 8)
+    uint32_t offset = 8;
+    uint32_t userid_start = offset;
+    while ( offset < len and data[offset] != 0 )
+    {
+        offset++;
+            if ( offset - userid_start > 255 )
+            return 0;
+    }
+
+    if ( offset >= len )
+        return 0;  // Need more data
+
+    offset++;  // Skip NULL terminator
+
+    // Check for SOCKS4a (IP is 0.0.0.x where x != 0)
+    uint32_t ip = (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
+    bool is_socks4a = (ip & 0xFFFFFF00) == 0 and (ip & 0xFF) != 0;
+
+    if ( is_socks4a )
+    {
+        // SOCKS4a: domain name follows, also NULL-terminated
+        uint32_t domain_start = offset;
+        while ( offset < len and data[offset] != 0 )
+        {
+            offset++;
+            // Prevent DoS: domain should be reasonable length (max 253 per RFC 1035)
+            if ( offset - domain_start > 253 )
+                return 0;
+        }
+
+        if ( offset >= len )
+            return 0;  // Need more data
+
+        offset++;  // Skip domain NULL terminator
+    }
+
+    return offset;
+}
+
+uint32_t SocksSplitter::parse_socks4_response(const uint8_t* data, uint32_t len)
+{
+    // SOCKS4 response: VER(1) STATUS(1) PORT(2) IP(4)
+    // Total: 8 bytes
+    // Note: VER is 0x00, not 0x04!
+    if ( len < 8 )
+        return 0;
+
+    if ( data[0] != 0x00 )
+        return 0;
+
+    return 8;
+}
diff --git a/src/service_inspectors/socks/socks_splitter.h b/src/service_inspectors/socks/socks_splitter.h
new file mode 100644 (file)
index 0000000..95eb9c8
--- /dev/null
@@ -0,0 +1,56 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_splitter.h author Raza Shafiq <rshafiq@cisco.com>
+
+#ifndef SOCKS_SPLITTER_H
+#define SOCKS_SPLITTER_H
+
+#include "stream/stream_splitter.h"
+#include "socks_flow_data.h"
+
+class SocksSplitter : public snort::StreamSplitter
+{
+public:
+    SocksSplitter(bool c2s);
+
+    Status scan(snort::Packet*, const uint8_t* data, uint32_t len,
+                uint32_t flags, uint32_t* fp) override;
+
+    bool is_paf() override { return true; }
+
+private:
+    uint32_t parse_client_packet(const uint8_t* data, uint32_t len, SocksState state);
+    uint32_t parse_server_packet(const uint8_t* data, uint32_t len, SocksState state);
+
+    // SOCKS4 parsing
+    uint32_t parse_socks4_request(const uint8_t* data, uint32_t len);
+    uint32_t parse_socks4_response(const uint8_t* data, uint32_t len);
+
+    // SOCKS5 parsing
+    uint32_t parse_auth_negotiation(const uint8_t* data, uint32_t len);
+    uint32_t parse_auth_response(const uint8_t* data, uint32_t len);
+    uint32_t parse_username_password_auth(const uint8_t* data, uint32_t len);
+    uint32_t parse_username_password_auth_response(const uint8_t* data, uint32_t len);
+    uint32_t parse_connect_request(const uint8_t* data, uint32_t len);
+    uint32_t parse_connect_response(const uint8_t* data, uint32_t len);
+
+    uint32_t parse_address_port_length(const uint8_t* data, uint32_t len, uint32_t atyp_offset);
+};
+
+#endif
diff --git a/src/service_inspectors/socks/test/CMakeLists.txt b/src/service_inspectors/socks/test/CMakeLists.txt
new file mode 100644 (file)
index 0000000..40f064e
--- /dev/null
@@ -0,0 +1,45 @@
+add_cpputest( socks_module_test
+    SOURCES
+        ../socks_module.cc
+        ../../../framework/module.cc
+)
+
+add_cpputest( socks_flow_data_test
+    SOURCES
+        ../socks_flow_data.cc
+        ../../../sfip/sf_ip.cc
+)
+
+add_cpputest( socks_handoff_test
+    SOURCES
+        ../socks_flow_data.cc
+)
+
+add_cpputest( socks_splitter_test
+    SOURCES
+        ../socks_splitter.cc
+        ../socks_flow_data.cc
+)
+
+add_cpputest( socks_negative_test
+    SOURCES
+        ../socks.cc
+        ../socks_module.cc
+        ../socks_splitter.cc
+        ../socks_flow_data.cc
+        ../../../sfip/sf_ip.cc
+        ../../../framework/module.cc
+)
+
+add_cpputest( socks_ips_test
+    SOURCES
+        socks_ips_test.cc
+        ../socks_flow_data.cc
+)
+
+add_cpputest( socks_splitter_negative_test
+    SOURCES
+        ../socks_splitter.cc
+        ../socks_flow_data.cc
+        ../../../sfip/sf_ip.cc
+)
diff --git a/src/service_inspectors/socks/test/socks_flow_data_test.cc b/src/service_inspectors/socks/test/socks_flow_data_test.cc
new file mode 100644 (file)
index 0000000..2fbc78b
--- /dev/null
@@ -0,0 +1,591 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_flow_data_test.cc author Raza Shafiq <rshafiq@cisco.com>
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "service_inspectors/socks/socks_flow_data.h"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+#undef malloc
+#undef free
+#undef calloc
+#undef realloc
+
+using namespace snort;
+
+// Stubs for linking
+namespace snort
+{
+FlowData::FlowData(unsigned u, Inspector* h) : handler(h), id(u)
+{ }
+
+unsigned FlowData::flow_data_id = 0;
+
+FlowData::~FlowData() = default;
+
+void* snort_alloc(size_t sz)
+{ return malloc(sz); }
+
+char* snort_strdup(const char* str)
+{
+    size_t n = strlen(str) + 1;
+    auto* p = static_cast<char*>(snort_alloc(n));
+    memcpy(p, str, n);
+    return p;
+}
+}
+
+// Mock for socks_stats - define struct locally to avoid header dependencies
+using PegCount = uint64_t;
+struct SocksStats
+{
+    PegCount sessions = 0;
+    PegCount concurrent_sessions = 0;
+    PegCount max_concurrent_sessions = 0;
+    PegCount auth_requests = 0;
+    PegCount auth_successes = 0;
+    PegCount failed_connections = 0;
+};
+THREAD_LOCAL SocksStats socks_stats;
+
+//-------------------------------------------------------------------------
+// SocksAddress Tests
+//-------------------------------------------------------------------------
+
+TEST_GROUP(socks_address_test)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksAddress* addr = nullptr;
+
+    void setup() override
+    {
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        addr = new SocksAddress();
+    }
+
+    void teardown() override
+    {
+        delete addr;
+    }
+};
+
+TEST(socks_address_test, constructor_defaults)
+{
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, addr->type);
+    CHECK_EQUAL(0, addr->port);
+    CHECK_TRUE(addr->address.empty());
+    CHECK_TRUE(addr->cached_ip == nullptr);
+    CHECK_FALSE(addr->is_set());
+}
+
+TEST(socks_address_test, set_domain_address)
+{
+    addr->set("example.com", SOCKS_ATYP_DOMAIN, 80);
+
+    CHECK_EQUAL(SOCKS_ATYP_DOMAIN, addr->type);
+    CHECK_EQUAL(80, addr->port);
+    CHECK_EQUAL(std::string("example.com"), addr->address);
+    CHECK_TRUE(addr->cached_ip == nullptr);
+    CHECK_TRUE(addr->is_set());
+
+    // Domain names don't convert to IP
+    auto ip_ptr = addr->get_ip();
+    CHECK_TRUE(ip_ptr == nullptr);
+}
+
+TEST(socks_address_test, set_ipv4_string)
+{
+    addr->set("192.168.1.100", SOCKS_ATYP_IPV4, 443);
+
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, addr->type);
+    CHECK_EQUAL(443, addr->port);
+    CHECK_EQUAL(std::string("192.168.1.100"), addr->address);
+    CHECK_TRUE(addr->cached_ip == nullptr);
+    CHECK_TRUE(addr->is_set());
+
+    // Lazy IP conversion
+    auto ip_ptr = addr->get_ip();
+    CHECK_TRUE(ip_ptr != nullptr);
+    CHECK_TRUE(addr->cached_ip != nullptr);
+    CHECK_TRUE(ip_ptr->is_ip4());
+}
+
+TEST(socks_address_test, set_ipv6_string)
+{
+    addr->set("2001:db8::1", SOCKS_ATYP_IPV6, 8080);
+
+    CHECK_EQUAL(SOCKS_ATYP_IPV6, addr->type);
+    CHECK_EQUAL(8080, addr->port);
+    CHECK_EQUAL(std::string("2001:db8::1"), addr->address);
+    CHECK_TRUE(addr->cached_ip == nullptr);
+
+    auto ip_ptr = addr->get_ip();
+    CHECK_TRUE(ip_ptr != nullptr);
+    CHECK_TRUE(addr->cached_ip != nullptr);
+    CHECK_TRUE(ip_ptr->is_ip6());
+}
+
+TEST(socks_address_test, set_from_binary_ipv4)
+{
+    SfIp test_ip;
+    test_ip.set("10.0.0.1");
+
+    addr->set(test_ip, 1080);
+
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, addr->type);
+    CHECK_EQUAL(1080, addr->port);
+    CHECK_EQUAL(std::string("10.0.0.1"), addr->address);
+    CHECK_TRUE(addr->cached_ip != nullptr);
+
+    auto ip_ptr = addr->get_ip();
+    CHECK_TRUE(ip_ptr != nullptr);
+    CHECK_TRUE(ip_ptr->is_ip4());
+}
+
+TEST(socks_address_test, set_from_binary_ipv6)
+{
+    SfIp test_ip;
+    test_ip.set("fe80::1");
+    
+    addr->set(test_ip, 9050);
+    
+    CHECK_EQUAL(SOCKS_ATYP_IPV6, addr->type);
+    CHECK_EQUAL(9050, addr->port);
+    CHECK_TRUE(addr->address.find("fe80") != std::string::npos);
+    CHECK_TRUE(addr->cached_ip != nullptr);
+    
+    auto ip_ptr = addr->get_ip();
+    CHECK_TRUE(ip_ptr != nullptr);
+    CHECK_TRUE(ip_ptr->is_ip6());
+}
+
+TEST(socks_address_test, clear_address)
+{
+    addr->set("192.168.1.1", SOCKS_ATYP_IPV4, 80);
+    addr->get_ip(); // Cache the IP
+    CHECK_TRUE(addr->cached_ip != nullptr);
+    
+    addr->clear();
+    
+    CHECK_TRUE(addr->address.empty());
+    CHECK_EQUAL(0, addr->port);
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, addr->type);
+    CHECK_TRUE(addr->cached_ip == nullptr);
+    CHECK_FALSE(addr->is_set());
+}
+
+TEST(socks_address_test, ip_caching_mechanism)
+{
+    addr->set("172.16.0.1", SOCKS_ATYP_IPV4, 22);
+
+    // First call should cache
+    CHECK_TRUE(addr->cached_ip == nullptr);
+    auto ip_ptr1 = addr->get_ip();
+    CHECK_TRUE(ip_ptr1 != nullptr);
+    CHECK_TRUE(addr->cached_ip != nullptr);
+
+    // Second call should return cached value
+    auto ip_ptr2 = addr->get_ip();
+    CHECK_TRUE(ip_ptr2 != nullptr);
+    // Both should have the same cached value
+}
+
+TEST(socks_address_test, invalid_ip_string)
+{
+    addr->set("not.an.ip.address", SOCKS_ATYP_IPV4, 80);
+    
+    auto ip_ptr = addr->get_ip();
+    CHECK_TRUE(ip_ptr == nullptr);
+    CHECK_TRUE(addr->cached_ip == nullptr);
+}
+
+TEST(socks_address_test, port_boundaries)
+{
+    addr->set("10.0.0.1", SOCKS_ATYP_IPV4, 0);
+    CHECK_EQUAL(0, addr->port);
+    
+    addr->set("10.0.0.1", SOCKS_ATYP_IPV4, 65535);
+    CHECK_EQUAL(65535, addr->port);
+}
+
+TEST(socks_address_test, loopback_addresses)
+{
+    // IPv4 loopback
+    addr->set("127.0.0.1", SOCKS_ATYP_IPV4, 1080);
+    auto ip_ptr4 = addr->get_ip();
+    CHECK_TRUE(ip_ptr4 != nullptr);
+    CHECK_TRUE(ip_ptr4->is_ip4());
+    
+    // IPv6 loopback
+    addr->clear();
+    addr->set("::1", SOCKS_ATYP_IPV6, 1080);
+    auto ip_ptr6 = addr->get_ip();
+    CHECK_TRUE(ip_ptr6 != nullptr);
+    CHECK_TRUE(ip_ptr6->is_ip6());
+}
+
+TEST(socks_address_test, ipv4_mapped_ipv6)
+{
+    addr->set("::ffff:192.168.1.1", SOCKS_ATYP_IPV6, 80);
+    
+    auto ip_ptr = addr->get_ip();
+    CHECK_TRUE(ip_ptr != nullptr);
+}
+
+//-------------------------------------------------------------------------
+// SocksFlowData Tests
+//-------------------------------------------------------------------------
+
+TEST_GROUP(socks_flow_data_test)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+    }
+
+    void teardown() override
+    {
+        delete flow_data;
+    }
+};
+
+TEST(socks_flow_data_test, constructor_initialization)
+{
+    CHECK_EQUAL(SOCKS_STATE_INIT, flow_data->get_state());
+    CHECK_EQUAL(SOCKS_DIR_CLIENT_TO_SERVER, flow_data->get_direction());
+    CHECK_EQUAL(0, flow_data->get_socks_version());
+    CHECK_FALSE(flow_data->is_socks4a());
+    CHECK_EQUAL(SOCKS5_AUTH_NONE, flow_data->get_auth_method());
+    CHECK_EQUAL(SOCKS_CMD_CONNECT, flow_data->get_command());
+    CHECK_EQUAL(0, flow_data->get_request_count());
+    CHECK_EQUAL(0, flow_data->get_response_count());
+    CHECK_EQUAL(SOCKS5_REP_SUCCESS, flow_data->get_last_error());
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+}
+
+TEST(socks_flow_data_test, direction_toggle)
+{
+    CHECK_EQUAL(SOCKS_DIR_CLIENT_TO_SERVER, flow_data->get_direction());
+    
+    flow_data->set_direction(SOCKS_DIR_SERVER_TO_CLIENT);
+    CHECK_EQUAL(SOCKS_DIR_SERVER_TO_CLIENT, flow_data->get_direction());
+    
+    flow_data->set_direction(SOCKS_DIR_CLIENT_TO_SERVER);
+    CHECK_EQUAL(SOCKS_DIR_CLIENT_TO_SERVER, flow_data->get_direction());
+}
+
+TEST(socks_flow_data_test, socks4a_flag)
+{
+    CHECK_FALSE(flow_data->is_socks4a());
+    
+    flow_data->set_socks4a(true);
+    CHECK_TRUE(flow_data->is_socks4a());
+    
+    flow_data->set_socks4a(false);
+    CHECK_FALSE(flow_data->is_socks4a());
+}
+
+TEST(socks_flow_data_test, target_operations)
+{
+    flow_data->set_target("example.com", SOCKS_ATYP_DOMAIN, 80);
+    
+    CHECK_EQUAL(std::string("example.com"), flow_data->get_target_address());
+    CHECK_EQUAL(SOCKS_ATYP_DOMAIN, flow_data->get_address_type());
+    CHECK_EQUAL(80, flow_data->get_target_port());
+}
+
+TEST(socks_flow_data_test, target_ip_operations)
+{
+    SfIp test_ip;
+    test_ip.set("203.0.113.50");
+    
+    flow_data->set_target_ip(test_ip);
+    
+    auto retrieved_ip = flow_data->get_target_ip();
+    CHECK_TRUE(retrieved_ip != nullptr);
+    CHECK_TRUE(retrieved_ip->is_ip4());
+}
+
+TEST(socks_flow_data_test, bind_operations)
+{
+    flow_data->set_bind("192.168.1.1", SOCKS_ATYP_IPV4, 9050);
+    
+    CHECK_EQUAL(std::string("192.168.1.1"), flow_data->get_bind_address());
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, flow_data->get_bind_address_type());
+    CHECK_EQUAL(9050, flow_data->get_bind_port());
+}
+
+TEST(socks_flow_data_test, bind_ip_operations)
+{
+    SfIp test_ip;
+    test_ip.set("172.16.0.1");
+    
+    flow_data->set_bind_ip(test_ip);
+    
+    auto retrieved_ip = flow_data->get_bind_ip();
+    CHECK_TRUE(retrieved_ip != nullptr);
+    CHECK_TRUE(retrieved_ip->is_ip4());
+}
+
+TEST(socks_flow_data_test, counter_overflow_behavior)
+{
+    // Test counter behavior near max values
+    for (unsigned i = 0; i < 100; i++)
+        flow_data->increment_request_count();
+    
+    CHECK_EQUAL(100, flow_data->get_request_count());
+}
+
+TEST(socks_flow_data_test, handoff_flags)
+{
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+    
+    flow_data->set_handoff_pending(true);
+    CHECK_TRUE(flow_data->is_handoff_pending());
+    
+    flow_data->set_handoff_completed(true);
+    CHECK_TRUE(flow_data->is_handoff_completed());
+    
+    flow_data->set_handoff_pending(false);
+    flow_data->set_handoff_completed(false);
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+}
+
+TEST(socks_flow_data_test, userid_operations)
+{
+    CHECK_TRUE(flow_data->get_userid().empty());
+    
+    flow_data->set_userid("testuser");
+    CHECK_EQUAL(std::string("testuser"), flow_data->get_userid());
+    
+    flow_data->set_userid("admin123");
+    CHECK_EQUAL(std::string("admin123"), flow_data->get_userid());
+    
+    flow_data->set_userid("");
+    CHECK_TRUE(flow_data->get_userid().empty());
+}
+
+TEST(socks_flow_data_test, inspector_id_consistency)
+{
+    unsigned id1 = SocksFlowData::get_inspector_id();
+    unsigned id2 = SocksFlowData::get_inspector_id();
+    
+    CHECK_EQUAL(id1, id2);
+    CHECK(id1 > 0);
+}
+
+//-------------------------------------------------------------------------
+// Integration Tests - Complex Scenarios
+//-------------------------------------------------------------------------
+
+TEST(socks_flow_data_test, socks5_connect_complete_workflow)
+{
+    // Simulate complete SOCKS5 CONNECT workflow
+    flow_data->set_socks_version(SOCKS5_VERSION);
+    flow_data->set_state(SOCKS_STATE_V5_AUTH_NEGOTIATION);
+    flow_data->set_auth_method(SOCKS5_AUTH_NONE);
+    flow_data->increment_request_count();
+    
+    flow_data->set_state(SOCKS_STATE_V5_CONNECT_REQUEST);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target("example.com", SOCKS_ATYP_DOMAIN, 80);
+    flow_data->increment_request_count();
+    
+    flow_data->set_state(SOCKS_STATE_V5_CONNECT_RESPONSE);
+    flow_data->set_direction(SOCKS_DIR_SERVER_TO_CLIENT);
+    flow_data->increment_response_count();
+    
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_handoff_pending(true);
+    
+    // Verify final state
+    CHECK_EQUAL(SOCKS5_VERSION, flow_data->get_socks_version());
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+    CHECK_EQUAL(SOCKS_CMD_CONNECT, flow_data->get_command());
+    CHECK_EQUAL(std::string("example.com"), flow_data->get_target_address());
+    CHECK_EQUAL(80, flow_data->get_target_port());
+    CHECK_EQUAL(2, flow_data->get_request_count());
+    CHECK_EQUAL(1, flow_data->get_response_count());
+    CHECK_TRUE(flow_data->is_handoff_pending());
+}
+
+TEST(socks_flow_data_test, socks5_udp_associate_workflow)
+{
+    flow_data->set_socks_version(SOCKS5_VERSION);
+    flow_data->set_command(SOCKS_CMD_UDP_ASSOCIATE);
+    flow_data->set_target("0.0.0.0", SOCKS_ATYP_IPV4, 0);
+    flow_data->set_bind("10.0.0.1", SOCKS_ATYP_IPV4, 5060);
+    
+    CHECK_EQUAL(SOCKS_CMD_UDP_ASSOCIATE, flow_data->get_command());
+}
+
+TEST(socks_flow_data_test, socks4a_domain_workflow)
+{
+    flow_data->set_socks_version(SOCKS4_VERSION);
+    flow_data->set_socks4a(true);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target("example.org", SOCKS_ATYP_DOMAIN, 443);
+    flow_data->set_userid("anonymous");
+    
+    CHECK_TRUE(flow_data->is_socks4a());
+    CHECK_EQUAL(std::string("example.org"), flow_data->get_target_address());
+    CHECK_EQUAL(443, flow_data->get_target_port());
+}
+
+TEST(socks_flow_data_test, username_password_auth_workflow)
+{
+    flow_data->set_socks_version(SOCKS5_VERSION);
+    flow_data->set_state(SOCKS_STATE_V5_AUTH_NEGOTIATION);
+    flow_data->set_auth_method(SOCKS5_AUTH_USERNAME_PASSWORD);
+    flow_data->increment_request_count();
+    
+    flow_data->set_state(SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH);
+    flow_data->set_userid("secureuser");
+    flow_data->increment_request_count();
+    
+    flow_data->set_state(SOCKS_STATE_V5_CONNECT_REQUEST);
+    flow_data->increment_request_count();
+    
+    CHECK_EQUAL(SOCKS5_AUTH_USERNAME_PASSWORD, flow_data->get_auth_method());
+    CHECK_EQUAL(std::string("secureuser"), flow_data->get_userid());
+    CHECK_EQUAL(3, flow_data->get_request_count());
+}
+
+TEST(socks_flow_data_test, ipv6_target_workflow)
+{
+    SfIp ipv6_target;
+    ipv6_target.set("2001:db8:85a3::8a2e:370:7334");
+    
+    flow_data->set_socks_version(SOCKS5_VERSION);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target_ip(ipv6_target);
+    flow_data->set_target_port(443);
+    
+    auto retrieved_ip = flow_data->get_target_ip();
+    CHECK_TRUE(retrieved_ip != nullptr);
+    CHECK_TRUE(retrieved_ip->is_ip6());
+    CHECK_EQUAL(SOCKS_ATYP_IPV6, flow_data->get_address_type());
+}
+
+TEST(socks_flow_data_test, handoff_lifecycle)
+{
+    // Test complete handoff lifecycle
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+    
+    // Connection established, ready for handoff
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_handoff_pending(true);
+    CHECK_TRUE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+    
+    // Handoff completed
+    flow_data->set_handoff_completed(true);
+    CHECK_TRUE(flow_data->is_handoff_completed());
+}
+
+TEST(socks_flow_data_test, empty_target_address)
+{
+    // Test behavior with empty target
+    CHECK_TRUE(flow_data->get_target_address().empty());
+    CHECK_EQUAL(0, flow_data->get_target_port());
+    
+    auto ip_ptr = flow_data->get_target_ip();
+    CHECK_TRUE(ip_ptr == nullptr);
+}
+
+TEST(socks_flow_data_test, empty_bind_address)
+{
+    // Test behavior with empty bind
+    CHECK_TRUE(flow_data->get_bind_address().empty());
+    CHECK_EQUAL(0, flow_data->get_bind_port());
+    
+    auto ip_ptr = flow_data->get_bind_ip();
+    CHECK_TRUE(ip_ptr == nullptr);
+}
+
+TEST(socks_flow_data_test, all_address_types)
+{
+    const SocksAddressType types[] = {
+        SOCKS_ATYP_IPV4,
+        SOCKS_ATYP_DOMAIN,
+        SOCKS_ATYP_IPV6
+    };
+    
+    for (auto type : types)
+    {
+        flow_data->set_address_type(type);
+        CHECK_EQUAL(type, flow_data->get_address_type());
+    }
+}
+
+TEST(socks_flow_data_test, stress_test_counters)
+{
+    // Stress test with large counter values
+    for (unsigned i = 0; i < 10000; i++)
+    {
+        flow_data->increment_request_count();
+        if (i % 2 == 0)
+            flow_data->increment_response_count();
+    }
+    
+    CHECK_EQUAL(10000, flow_data->get_request_count());
+    CHECK_EQUAL(5000, flow_data->get_response_count());
+}
+
+TEST(socks_flow_data_test, private_ip_ranges)
+{
+    // Test with various private IP ranges
+    const char* private_ips[] = {
+        "10.0.0.1",
+        "172.16.0.1",
+        "192.168.1.1",
+        "127.0.0.1"
+    };
+    
+    for (const char* const ip_str : private_ips)
+    {
+        SfIp test_ip;
+        test_ip.set(ip_str);
+        flow_data->set_target_ip(test_ip);
+        
+        auto retrieved_ip = flow_data->get_target_ip();
+        CHECK_TRUE(retrieved_ip != nullptr);
+        CHECK_TRUE(retrieved_ip->is_ip4());
+    }
+}
+
+int main(int argc, char** argv)
+{
+    MemoryLeakWarningPlugin::turnOffNewDeleteOverloads();
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
diff --git a/src/service_inspectors/socks/test/socks_handoff_test.cc b/src/service_inspectors/socks/test/socks_handoff_test.cc
new file mode 100644 (file)
index 0000000..503172d
--- /dev/null
@@ -0,0 +1,202 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_handoff_test.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "service_inspectors/socks/socks_flow_data.h"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+#undef malloc
+#undef free
+#undef calloc
+#undef realloc
+
+using namespace snort;
+
+// Stubs for linking
+namespace snort
+{
+FlowData::FlowData(unsigned u, Inspector* h) : handler(h), id(u) {}
+unsigned FlowData::flow_data_id = 0;
+FlowData::~FlowData() = default;
+
+} // namespace snort
+
+// Mock for socks_stats - define struct locally to avoid header dependencies
+using PegCount = uint64_t;
+struct SocksStats
+{
+    PegCount sessions = 0;
+    PegCount concurrent_sessions = 0;
+    PegCount max_concurrent_sessions = 0;
+    PegCount auth_requests = 0;
+    PegCount auth_successes = 0;
+    PegCount failed_connections = 0;
+};
+THREAD_LOCAL SocksStats socks_stats;
+
+//-------------------------------------------------------------------------
+// Handoff Tests
+//-------------------------------------------------------------------------
+
+TEST_GROUP(socks_handoff_test)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+    }
+
+    void teardown() override
+    {
+        delete flow_data;
+    }
+};
+
+TEST(socks_handoff_test, initial_handoff_state)
+{
+    // Verify initial state
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+}
+
+TEST(socks_handoff_test, handoff_lifecycle_connect)
+{
+    // Simulate CONNECT command handoff lifecycle
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    
+    // Initial state
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+    
+    // Set pending after connection established
+    flow_data->set_handoff_pending(true);
+    CHECK_TRUE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+    
+    // Complete handoff
+    flow_data->set_handoff_pending(false);
+    flow_data->set_handoff_completed(true);
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_TRUE(flow_data->is_handoff_completed());
+}
+
+TEST(socks_handoff_test, no_handoff_for_bind)
+{
+    // BIND command should not trigger handoff
+    flow_data->set_command(SOCKS_CMD_BIND);
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+}
+
+TEST(socks_handoff_test, no_handoff_for_udp_associate)
+{
+    // UDP_ASSOCIATE command should not trigger handoff
+    flow_data->set_command(SOCKS_CMD_UDP_ASSOCIATE);
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+}
+
+TEST(socks_handoff_test, handoff_with_target_info)
+{
+    // Test handoff with target address information
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target("example.com", SOCKS_ATYP_DOMAIN, 443);
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_handoff_pending(true);
+    
+    CHECK_TRUE(flow_data->is_handoff_pending());
+    CHECK_EQUAL(std::string("example.com"), flow_data->get_target_address());
+    CHECK_EQUAL(443, flow_data->get_target_port());
+}
+
+TEST(socks_handoff_test, socks4_connect_handoff)
+{
+    // Test SOCKS4 CONNECT handoff
+    flow_data->set_socks_version(SOCKS4_VERSION);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target("10.0.0.1", SOCKS_ATYP_IPV4, 22);
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_handoff_pending(true);
+    
+    CHECK_EQUAL(SOCKS4_VERSION, flow_data->get_socks_version());
+    CHECK_TRUE(flow_data->is_handoff_pending());
+}
+
+TEST(socks_handoff_test, socks4a_connect_handoff)
+{
+    // Test SOCKS4a CONNECT handoff with domain
+    flow_data->set_socks_version(SOCKS4_VERSION);
+    flow_data->set_socks4a(true);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target("ssh.example.com", SOCKS_ATYP_DOMAIN, 22);
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_handoff_pending(true);
+    
+    CHECK_TRUE(flow_data->is_socks4a());
+    CHECK_TRUE(flow_data->is_handoff_pending());
+    CHECK_EQUAL(std::string("ssh.example.com"), flow_data->get_target_address());
+}
+
+TEST(socks_handoff_test, handoff_with_auth)
+{
+    // Test handoff after authentication
+    flow_data->set_socks_version(SOCKS5_VERSION);
+    flow_data->set_auth_method(SOCKS5_AUTH_USERNAME_PASSWORD);
+    flow_data->set_userid("testuser");
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target("secure.example.com", SOCKS_ATYP_DOMAIN, 443);
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_handoff_pending(true);
+    
+    CHECK_EQUAL(SOCKS5_AUTH_USERNAME_PASSWORD, flow_data->get_auth_method());
+    CHECK_TRUE(flow_data->is_handoff_pending());
+}
+
+TEST(socks_handoff_test, handoff_with_error_state)
+{
+    // Test that handoff doesn't occur in error state
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_state(SOCKS_STATE_ERROR);
+    flow_data->set_last_error(SOCKS5_REP_CONNECTION_REFUSED);
+    
+    CHECK_FALSE(flow_data->is_handoff_pending());
+    CHECK_FALSE(flow_data->is_handoff_completed());
+}
+
+
+int main(int argc, char** argv)
+{
+    MemoryLeakWarningPlugin::turnOffNewDeleteOverloads();
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
diff --git a/src/service_inspectors/socks/test/socks_ips_test.cc b/src/service_inspectors/socks/test/socks_ips_test.cc
new file mode 100644 (file)
index 0000000..f87b5d0
--- /dev/null
@@ -0,0 +1,294 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_ips_test.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <string>
+
+#include "../socks_flow_data.h"
+#include "framework/cursor.h"
+#include "framework/ips_option.h"
+#include "framework/module.h"
+#include "hash/hash_key_operations.h"
+#include "protocols/packet.h"
+#include "service_inspectors/socks/socks_module.h"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+using namespace snort;
+
+static snort::FlowData* stored_flow_data = nullptr;
+
+namespace snort
+{
+unsigned FlowData::flow_data_id = 0;
+
+FlowData::FlowData(unsigned u, Inspector* i) : handler(i), id(u) {}
+FlowData::~FlowData() = default;
+
+FlowDataStore::~FlowDataStore() = default;
+FlowData* FlowDataStore::get(unsigned) const { return stored_flow_data; }
+void FlowDataStore::set(FlowData* fd) { stored_flow_data = fd; }
+void FlowDataStore::erase(unsigned) {}
+void FlowDataStore::erase(FlowData*) {}
+void FlowDataStore::clear() {}
+bool FlowDataStore::empty() const { return stored_flow_data == nullptr; }
+void FlowDataStore::call_handlers(Packet*, FlowDataHandlerType) const {}
+
+Flow::~Flow() = default;
+
+// cppcheck-suppress uninitMemberVar ; mock class - only data/dsize used in tests
+Packet::Packet(bool) { }
+Packet::~Packet() = default;
+
+// cppcheck-suppress uninitMemberVar ; mock IpsOption does not init base members
+IpsOption::IpsOption(const char*, option_type_t) { }
+uint32_t IpsOption::hash() const { return 0; }
+bool IpsOption::operator==(const IpsOption&) const { return true; }
+uint16_t IpsOption::get_pdu_section(bool) const { return 0; }
+
+// cppcheck-suppress uninitMemberVar ; mock Module does not init base members
+Module::Module(const char*, const char*) { }
+// cppcheck-suppress uninitMemberVar ; mock Module does not init base members
+Module::Module(const char*, const char*, const Parameter*, bool) { }
+void Module::sum_stats(bool) { }
+void Module::show_interval_stats(std::vector<unsigned>&, FILE*) { }
+void Module::show_stats() { }
+void Module::init_stats(bool) { }
+void Module::reset_stats() { }
+void Module::main_accumulate_stats() { }
+PegCount Module::get_global_count(char const*) const { return 0; }
+
+uint64_t Parameter::get_uint(const char* r)
+{
+    bool ok = false;
+    return Parameter::get_uint(r, ok);
+}
+
+uint64_t Parameter::get_uint(const char* r, bool& is_correct)
+{
+    char* end = nullptr;
+    uint64_t value = strtoull(r, &end, 0);
+    is_correct = (end && *end == '\0');
+    return value;
+}
+
+void mix_str(uint32_t&, uint32_t&, uint32_t&, const char*, unsigned) { }
+
+THREAD_LOCAL bool TimeProfilerStats::enabled = false;
+}
+
+THREAD_LOCAL SocksStats socks_stats = {};
+THREAD_LOCAL snort::ProfileStats socksPerfStats = {};
+
+#include "../socks_ips.cc"
+
+TEST_GROUP(SocksIpsStateTest)
+{
+    void setup() override
+    {
+        SocksFlowData::init();
+        stored_flow_data = nullptr;
+    }
+
+    void teardown() override
+    {
+        stored_flow_data = nullptr;
+    }
+};
+
+TEST(SocksIpsStateTest, no_flow_or_flow_data_no_match)
+{
+    SocksStateOption opt(SOCKS_STATE_CLASS_ESTABLISHED);
+    Cursor c;
+    Packet p(true);
+
+    p.flow = nullptr;
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+
+    snort::Flow f;
+    p.flow = &f;
+    stored_flow_data = nullptr;
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+}
+
+TEST(SocksIpsStateTest, state_class_mapping_matches)
+{
+    SocksStateOption opt_auth(SOCKS_STATE_CLASS_AUTH);
+    SocksStateOption opt_req(SOCKS_STATE_CLASS_REQUEST_RESPONSE);
+    SocksStateOption opt_est(SOCKS_STATE_CLASS_ESTABLISHED);
+    SocksStateOption opt_err(SOCKS_STATE_CLASS_ERROR);
+
+    SocksFlowData fd;
+    snort::Flow f;
+    f.set_flow_data(&fd);
+
+    Packet p(true);
+    p.flow = &f;
+    Cursor c;
+
+    fd.set_state(SOCKS_STATE_INIT);
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt_auth.eval(c, &p));
+
+    fd.set_state(SOCKS_STATE_V5_AUTH_NEGOTIATION);
+    CHECK_EQUAL(IpsOption::MATCH, opt_auth.eval(c, &p));
+
+    fd.set_state(SOCKS_STATE_V5_CONNECT_RESPONSE);
+    CHECK_EQUAL(IpsOption::MATCH, opt_req.eval(c, &p));
+
+    fd.set_state(SOCKS_STATE_ESTABLISHED);
+    CHECK_EQUAL(IpsOption::MATCH, opt_est.eval(c, &p));
+
+    fd.set_state(SOCKS_STATE_ERROR);
+    CHECK_EQUAL(IpsOption::MATCH, opt_err.eval(c, &p));
+}
+
+TEST(SocksIpsStateTest, parses_named_and_numeric_states)
+{
+    SocksStateModule mod;
+    Value v_named("established");
+    v_named.set(&socks_state_params[0]);
+    CHECK_TRUE(mod.set(nullptr, v_named, nullptr));
+    CHECK_EQUAL(SOCKS_STATE_CLASS_ESTABLISHED, mod.state_class);
+
+    Value v_numeric("3");
+    v_numeric.set(&socks_state_params[0]);
+    CHECK_TRUE(mod.set(nullptr, v_numeric, nullptr));
+    CHECK_EQUAL(SOCKS_STATE_CLASS_ESTABLISHED, mod.state_class);
+
+    Value v_invalid("0");
+    v_invalid.set(&socks_state_params[0]);
+    CHECK_FALSE(mod.set(nullptr, v_invalid, nullptr));
+}
+
+TEST(SocksIpsStateTest, socks_version_matches)
+{
+    SocksVersionOption opt(SOCKS5_VERSION);
+    Cursor c;
+    Packet p(true);
+
+    SocksFlowData fd;
+    snort::Flow f;
+    f.set_flow_data(&fd);
+    p.flow = &f;
+
+    fd.set_socks_version(SOCKS5_VERSION);
+    CHECK_EQUAL(IpsOption::MATCH, opt.eval(c, &p));
+
+    fd.set_socks_version(SOCKS4_VERSION);
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+}
+
+TEST(SocksIpsStateTest, socks_command_matches_when_target_set)
+{
+    SocksCommandOption opt(SOCKS_CMD_CONNECT);
+    Cursor c;
+    Packet p(true);
+
+    SocksFlowData fd;
+    snort::Flow f;
+    f.set_flow_data(&fd);
+    p.flow = &f;
+
+    fd.set_command(SOCKS_CMD_CONNECT);
+    fd.set_target_address("10.0.0.1");
+    CHECK_EQUAL(IpsOption::MATCH, opt.eval(c, &p));
+
+    fd.set_command(SOCKS_CMD_BIND);
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+
+    fd.set_command(SOCKS_CMD_CONNECT);
+    fd.set_target_address("");
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+}
+
+TEST(SocksIpsStateTest, socks_address_type_matches_when_target_set)
+{
+    Socks5AddressTypeOption opt(SOCKS_ATYP_DOMAIN);
+    Cursor c;
+    Packet p(true);
+
+    SocksFlowData fd;
+    snort::Flow f;
+    f.set_flow_data(&fd);
+    p.flow = &f;
+
+    fd.set_target("example.com", SOCKS_ATYP_DOMAIN, 80);
+    CHECK_EQUAL(IpsOption::MATCH, opt.eval(c, &p));
+
+    fd.set_address_type(SOCKS_ATYP_IPV4);
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+
+    fd.set_target_address("");
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+}
+
+TEST(SocksIpsStateTest, socks_remote_address_matches_and_sets_cursor)
+{
+    SocksRemoteAddressOption opt_match("example");
+    SocksRemoteAddressOption opt_cursor;
+    Cursor c;
+    Packet p(true);
+
+    SocksFlowData fd;
+    snort::Flow f;
+    f.set_flow_data(&fd);
+    p.flow = &f;
+
+    const std::string addr = "bad.example.com";
+    fd.set_target_address(addr);
+
+    CHECK_EQUAL(IpsOption::MATCH, opt_match.eval(c, &p));
+
+    CHECK_EQUAL(IpsOption::MATCH, opt_cursor.eval(c, &p));
+    CHECK_TRUE(c.get_name() != nullptr);
+    CHECK_EQUAL(0, strcmp(c.get_name(), "socks_remote_address"));
+    CHECK_EQUAL(addr.size(), c.size());
+    const std::string buf(reinterpret_cast<const char*>(c.buffer()), c.size());
+    CHECK_EQUAL(addr, buf);
+}
+
+TEST(SocksIpsStateTest, socks_remote_port_matches)
+{
+    SocksRemotePortOption opt(443);
+    Cursor c;
+    Packet p(true);
+
+    SocksFlowData fd;
+    snort::Flow f;
+    f.set_flow_data(&fd);
+    p.flow = &f;
+
+    fd.set_target_port(443);
+    CHECK_EQUAL(IpsOption::MATCH, opt.eval(c, &p));
+
+    fd.set_target_port(80);
+    CHECK_EQUAL(IpsOption::NO_MATCH, opt.eval(c, &p));
+}
+
+int main(int argc, char** argv)
+{
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
diff --git a/src/service_inspectors/socks/test/socks_module_test.cc b/src/service_inspectors/socks/test/socks_module_test.cc
new file mode 100644 (file)
index 0000000..68fb9f6
--- /dev/null
@@ -0,0 +1,218 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_module_test.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "log/messages.h"
+#include "main/thread_config.h"
+#include "service_inspectors/socks/socks_module.h"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+#include <CppUTestExt/MockSupport.h>
+
+using namespace snort;
+
+namespace snort
+{
+// Stubs whose sole purpose is to make the test code link
+void ParseWarning(WarningGroup, const char*, ...) {}
+void ParseError(const char*, ...) {}
+
+unsigned get_instance_id()
+{ return 0; }
+unsigned ThreadConfig::get_instance_max() { return 1; }
+}
+
+void show_stats(PegCount*, const PegInfo*, unsigned, const char*) { }
+void show_stats(PegCount*, const PegInfo*, const std::vector<unsigned>&, const char*, FILE*) { }
+
+// THREAD_LOCAL variables that socks_module.cc references
+THREAD_LOCAL ProfileStats socksPerfStats;
+THREAD_LOCAL SocksStats socks_stats;
+
+// Stub class and constructor for Socks5Inspector to resolve linking
+class SocksInspector
+{
+public:
+    SocksInspector(const SocksModule*);
+    ~SocksInspector();
+};
+
+// Provide explicit constructor and destructor definitions
+SocksInspector::SocksInspector(const SocksModule*) {}
+SocksInspector::~SocksInspector() {}
+
+TEST_GROUP(socks5_module_test)
+{
+    SocksModule* mod = nullptr;
+
+    void setup() override
+    {
+        mod = new SocksModule();
+        (void)mod;
+    }
+
+    void teardown() override
+    {
+        // Reset stats to zero
+        PegCount* counts = mod->get_counts();
+        for (unsigned k = 0; k < SOCKS_PEG_MAX; k++)
+        {
+            counts[k] = 0;
+        }
+        delete mod;
+    }
+};
+
+TEST(socks5_module_test, constructor_defaults)
+{
+    CHECK_EQUAL(Module::INSPECT, mod->get_usage());
+    CHECK_TRUE(mod->is_bindable());
+    CHECK(mod->get_pegs() == socks_pegs);
+    CHECK(mod->get_counts() != nullptr);
+    CHECK(mod->get_profile() == &socksPerfStats);
+}
+
+TEST(socks5_module_test, peg_count_initialization)
+{
+    // Test that all peg counts are initialized to zero
+    for (unsigned k = 0; k < SOCKS_PEG_MAX; k++)
+    {
+        CHECK_EQUAL(0, socks_stats.sessions);
+        CHECK_EQUAL(0, socks_stats.concurrent_sessions);
+        CHECK_EQUAL(0, socks_stats.auth_requests);
+        break; // Just test a few key ones
+    }
+}
+
+TEST(socks5_module_test, module_interface)
+{
+    // Test basic module interface
+    CHECK(mod != nullptr);
+    CHECK_TRUE(mod->is_bindable());
+    CHECK_EQUAL(Module::INSPECT, mod->get_usage());
+}
+
+TEST(socks5_module_test, lifecycle_methods)
+{
+    // Test module lifecycle methods
+    CHECK_TRUE(mod->begin("", 0, nullptr));
+    CHECK_TRUE(mod->end("", 0, nullptr));
+}
+
+TEST(socks5_module_test, peg_counters_available)
+{
+    const PegInfo* pegs = mod->get_pegs();
+    CHECK(pegs != nullptr);
+    CHECK(pegs == socks_pegs);
+    
+    // Verify all expected pegs are present
+    int count = 0;
+    while (pegs[count].type != CountType::END)
+    {
+        count++;
+    }
+    CHECK_EQUAL(SOCKS_PEG_MAX, count);
+}
+
+TEST(socks5_module_test, all_stats_peg_mapping)
+{
+    PegCount* counts = mod->get_counts();
+    CHECK(counts != nullptr);
+    
+    // Set all stats to unique values and verify peg index mapping
+    socks_stats.sessions = 1;
+    socks_stats.concurrent_sessions = 2;
+    socks_stats.max_concurrent_sessions = 3;
+    socks_stats.auth_requests = 4;
+    socks_stats.auth_successes = 5;
+    socks_stats.auth_failures = 6;
+    socks_stats.connect_requests = 7;
+    socks_stats.bind_requests = 8;
+    socks_stats.udp_associate_requests = 9;
+    socks_stats.successful_connections = 10;
+    socks_stats.failed_connections = 11;
+    socks_stats.udp_associations_created = 12;
+    socks_stats.udp_expectations_created = 13;
+    socks_stats.udp_packets = 14;
+    socks_stats.udp_frags_dropped = 15;
+    socks_stats.udp_frags_blocked = 16;
+    
+    // Verify all peg indices map correctly
+    CHECK_EQUAL(1, counts[SOCKS_PEG_SESSIONS]);
+    CHECK_EQUAL(2, counts[SOCKS_PEG_CONCURRENT_SESSIONS]);
+    CHECK_EQUAL(3, counts[SOCKS_PEG_MAX_CONCURRENT_SESSIONS]);
+    CHECK_EQUAL(4, counts[SOCKS_PEG_AUTH_REQUESTS]);
+    CHECK_EQUAL(5, counts[SOCKS_PEG_AUTH_SUCCESSES]);
+    CHECK_EQUAL(6, counts[SOCKS_PEG_AUTH_FAILURES]);
+    CHECK_EQUAL(7, counts[SOCKS_PEG_CONNECT_REQUESTS]);
+    CHECK_EQUAL(8, counts[SOCKS_PEG_BIND_REQUESTS]);
+    CHECK_EQUAL(9, counts[SOCKS_PEG_UDP_ASSOCIATE_REQUESTS]);
+    CHECK_EQUAL(10, counts[SOCKS_PEG_SUCCESSFUL_CONNECTIONS]);
+    CHECK_EQUAL(11, counts[SOCKS_PEG_FAILED_CONNECTIONS]);
+    CHECK_EQUAL(12, counts[SOCKS_PEG_UDP_ASSOCIATIONS_CREATED]);
+    CHECK_EQUAL(13, counts[SOCKS_PEG_UDP_EXPECTATIONS_CREATED]);
+    CHECK_EQUAL(14, counts[SOCKS_PEG_UDP_PACKETS]);
+    CHECK_EQUAL(15, counts[SOCKS_PEG_UDP_FRAGS_DROPPED]);
+    CHECK_EQUAL(16, counts[SOCKS_PEG_UDP_FRAGS_BLOCKED]);
+}
+
+TEST(socks5_module_test, basic_functionality)
+{
+    // Test basic module functionality without external dependencies
+    CHECK(mod != nullptr);
+    CHECK_TRUE(mod->is_bindable());
+}
+
+TEST(socks5_module_test, module_constants)
+{
+    // Test module constants are properly defined
+    CHECK(strcmp(SOCKS_NAME, "socks") == 0);
+    CHECK(strcmp(SOCKS_HELP, "SOCKS protocol inspector") == 0);
+}
+
+TEST(socks5_module_test, all_pegs_defined)
+{
+    // Ensure all peg info entries are properly defined
+    const PegInfo* pegs = socks_pegs;
+    
+    // Check first few entries
+    CHECK(pegs[SOCKS_PEG_SESSIONS].type == CountType::SUM);
+    CHECK(strcmp(pegs[SOCKS_PEG_SESSIONS].name, "sessions") == 0);
+    
+    CHECK(pegs[SOCKS_PEG_CONCURRENT_SESSIONS].type == CountType::NOW);
+    CHECK(strcmp(pegs[SOCKS_PEG_CONCURRENT_SESSIONS].name, "concurrent_sessions") == 0);
+    
+    CHECK(pegs[SOCKS_PEG_MAX_CONCURRENT_SESSIONS].type == CountType::MAX);
+    CHECK(strcmp(pegs[SOCKS_PEG_MAX_CONCURRENT_SESSIONS].name, "max_concurrent_sessions") == 0);
+    
+    // Check last entry is properly terminated
+    CHECK(pegs[SOCKS_PEG_MAX].type == CountType::END);
+    CHECK(pegs[SOCKS_PEG_MAX].name == nullptr);
+    CHECK(pegs[SOCKS_PEG_MAX].help == nullptr);
+}
+
+int main(int argc, char** argv)
+{
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
diff --git a/src/service_inspectors/socks/test/socks_negative_test.cc b/src/service_inspectors/socks/test/socks_negative_test.cc
new file mode 100644 (file)
index 0000000..bc62e86
--- /dev/null
@@ -0,0 +1,2033 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_negative_test.cc author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "../socks_flow_data.h"
+#include "../socks_module.h"
+#include "../socks.h"
+
+#include "detection/detection_engine.h"
+#include "packet_io/packet_tracer.h"
+#include "packet_io/active.h"
+#include "framework/data_bus.h"
+#include "stream/stream.h"
+#include "stream/stream_splitter.h"
+#include "main/analyzer.h"
+#include "main/thread_config.h"
+#include "main/snort_config.h"
+#include "log/unified2.h"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+using namespace snort;
+
+// Test helper class - exposes protected methods for testing
+class SocksInspectorTestHelper : public SocksInspector
+{
+public:
+    SocksInspectorTestHelper(const SocksModule* mod) : SocksInspector(mod) {}
+    
+    // Expose protected methods as public for testing
+    using SocksInspector::parse_socks4_request;
+    using SocksInspector::parse_socks4_response;
+    using SocksInspector::parse_socks5_auth_response;
+    using SocksInspector::validate_socks5_request_header;
+    using SocksInspector::process_client_data;
+    using SocksInspector::process_server_data;
+    using SocksInspector::process_reverse_client_data;
+    using SocksInspector::process_reverse_server_data;
+    using SocksInspector::detect_protocol_initiator;
+};
+
+// Mock implementations
+namespace snort
+{
+    unsigned FlowData::flow_data_id = 0;
+    
+    FlowData::FlowData(unsigned u, Inspector* i) : handler(i), id(u) {}
+    FlowData::~FlowData() = default;
+    
+    char* snort_strdup(const char* s)
+    {
+        if (!s)
+            return nullptr;
+        size_t len = strlen(s) + 1;
+        char* dup = new char[len];
+        memcpy(dup, s, len);
+        return dup;
+    }
+    
+    // Mock Snort framework functions
+    Inspector::Inspector() = default;
+    Inspector::~Inspector() = default;
+    void Inspector::rem_ref() {}
+    bool Inspector::likes(Packet*) { return true; }
+    bool Inspector::get_buf(const char*, Packet*, InspectionBuffer&) { return false; }
+    
+    // cppcheck-suppress uninitMemberVar ; mock class - only data/dsize used in tests
+    Packet::Packet(bool) {}
+    Packet::~Packet() = default;
+    
+    void Flow::set_service(Packet*, const char*) {}
+    
+    // Simple mock FlowDataStore that stores one FlowData
+    static FlowData* stored_flow_data = nullptr;
+    FlowData* FlowDataStore::get(unsigned) const { return stored_flow_data; }
+    void FlowDataStore::set(FlowData* fd) { stored_flow_data = fd; }
+    FlowDataStore::~FlowDataStore() = default;
+    
+    // Helper to clear stored flow data (prevents use-after-free in tests)
+    static void clear_stored_flow_data() { stored_flow_data = nullptr; }
+    
+    unsigned get_instance_id() { return 0; }
+    
+    // Mock DetectionEngine
+    Packet* DetectionEngine::get_current_packet() { return nullptr; }
+    int DetectionEngine::queue_event(unsigned, unsigned) { return 0; }
+    Packet* DetectionEngine::set_next_packet(const Packet*, Flow*) { return nullptr; }
+    DetectionEngine::DetectionEngine() = default;
+    DetectionEngine::~DetectionEngine() = default;
+    void DetectionEngine::set_encode_packet(Packet*) {}
+    void DetectionEngine::disable_all(Packet*) {}
+    
+    // Mock Active
+    void Active::block_session(Packet*, bool) {}
+    void Active::set_drop_reason(const char*) {}
+    
+    // Mock PacketTracer
+    bool PacketTracer::is_active() { return false; }
+    void PacketTracer::log(const char*, ...) {}
+    
+    // Mock DataBus
+    void DataBus::publish(unsigned, unsigned, DataEvent&, Flow*) {}
+    void DataBus::publish(unsigned, unsigned, Packet*, Flow*) {}
+    unsigned DataBus::get_id(const PubKey&) { return 0; }
+    
+    // Mock Stream
+    uint32_t Stream::reg_xtra_data_cb(int (*)(Flow*, uint8_t**, uint32_t*, uint32_t*)) { return 0; }
+    void Stream::set_extra_data(Flow*, Packet*, uint32_t) {}
+    int Stream::set_snort_protocol_id_expected(const Packet*, PktType, IpProtocol,
+        const SfIp*, uint16_t, const SfIp*, uint16_t, SnortProtocolId, FlowData*,
+        bool, bool, bool, bool) { return 0; }
+    uint32_t Stream::get_paf_position(Flow*, bool) { return 0; }
+    void Stream::set_splitter_with_rescan(Flow*, bool, StreamSplitter*, uint32_t) {}
+    
+    // Mock ProtocolReference
+    SnortProtocolId ProtocolReference::add(const char*) { return 1; }
+    
+    // Mock ThreadConfig
+    unsigned ThreadConfig::get_instance_max() { return 1; }
+    
+    // Mock Inspector vtable
+    StreamSplitter* Inspector::get_splitter(bool) { return nullptr; }
+    
+    // Mock Config Logger
+    namespace ConfigLogger
+    {
+        void log_flag(const char*, bool, bool) {}
+    }
+    
+    // Mock StreamSplitter pure virtual methods
+    const StreamBuffer StreamSplitter::reassemble(Flow*, unsigned, unsigned, const uint8_t*, unsigned, uint32_t, unsigned&)
+    {
+        return StreamBuffer();
+    }
+    unsigned StreamSplitter::max(Flow*) { return 0; }
+    
+    // Mock Flow destructor
+    Flow::~Flow() = default;
+    
+    // Mock Profiler
+    THREAD_LOCAL bool TimeProfilerStats::enabled = false;
+}
+
+// Mock Analyzer
+Analyzer* Analyzer::get_local_analyzer() { return nullptr; }
+bool Analyzer::process_rebuilt_packet(Packet*, const DAQ_PktHdr_t*, const uint8_t*, uint32_t) { return true; }
+
+void show_stats(PegCount*, const PegInfo*, unsigned, const char*) {}
+void show_stats(PegCount*, const PegInfo*, const std::vector<unsigned>&, const char*, FILE*) {}
+
+// Mock IPS option symbols
+const BaseApi* ips_socks_version = nullptr;
+const BaseApi* ips_socks_state = nullptr;
+const BaseApi* ips_socks_command = nullptr;
+const BaseApi* ips_socks_address_type = nullptr;
+const BaseApi* ips_socks_remote_address = nullptr;
+const BaseApi* ips_socks_remote_port = nullptr;
+
+// Note: socks_stats is defined in socks_module.cc which is linked
+
+//=============================================================================
+// Test Group: SOCKS Flow Data Boundary Conditions
+//=============================================================================
+TEST_GROUP(SocksFlowDataBoundaryTests)
+{
+    void setup()
+    {
+        SocksFlowData::init();
+    }
+};
+
+// Test initial state - covers lines with default values
+TEST(SocksFlowDataBoundaryTests, test_initial_state)
+{
+    SocksFlowData fd;
+    
+    // State checks
+    CHECK_EQUAL(SOCKS_STATE_INIT, fd.get_state());
+    CHECK_EQUAL(SOCKS_INITIATOR_UNKNOWN, fd.get_initiator());
+    CHECK_EQUAL(SOCKS_DIR_CLIENT_TO_SERVER, fd.get_direction()); // Default per socks_flow_data.cc:40
+    
+    // Flags
+    CHECK_FALSE(fd.is_session_counted());
+    CHECK_FALSE(fd.is_handoff_pending());
+    CHECK_FALSE(fd.is_handoff_completed());
+    CHECK_FALSE(fd.is_socks4a());
+    
+    // Counters
+    CHECK_EQUAL(0, fd.get_request_count());
+    CHECK_EQUAL(0, fd.get_response_count());
+    
+    // Strings
+    CHECK_TRUE(fd.get_target_address().empty());
+    CHECK_TRUE(fd.get_bind_address().empty());
+    CHECK_TRUE(fd.get_userid().empty());
+    
+    // Ports
+    CHECK_EQUAL(0, fd.get_target_port());
+    CHECK_EQUAL(0, fd.get_bind_port());
+    
+    // Pointers - target_ip is nullptr initially
+    CHECK_TRUE(fd.get_target_ip() == nullptr);
+    // Note: udp_reassembly is created on first get_udp_reassembly() call
+}
+
+// Test all state transitions
+TEST(SocksFlowDataBoundaryTests, test_all_state_transitions)
+{
+    SocksFlowData fd;
+    
+    const SocksState states[] = {
+        SOCKS_STATE_INIT,
+        SOCKS_STATE_V4_CONNECT_RESPONSE,
+        SOCKS_STATE_V4_BIND_SECOND_RESPONSE,
+        SOCKS_STATE_V5_AUTH_NEGOTIATION,
+        SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH,
+        SOCKS_STATE_V5_CONNECT_REQUEST,
+        SOCKS_STATE_V5_CONNECT_RESPONSE,
+        SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION,
+        SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE,
+        SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST,
+        SOCKS_STATE_ESTABLISHED,
+        SOCKS_STATE_ERROR
+    };
+    
+    for (auto state : states)
+    {
+        fd.set_state(state);
+        CHECK_EQUAL(state, fd.get_state());
+    }
+}
+
+// Test all command values
+TEST(SocksFlowDataBoundaryTests, test_all_commands)
+{
+    SocksFlowData fd;
+    
+    fd.set_command(SOCKS_CMD_CONNECT);
+    CHECK_EQUAL(SOCKS_CMD_CONNECT, fd.get_command());
+    
+    fd.set_command(SOCKS_CMD_BIND);
+    CHECK_EQUAL(SOCKS_CMD_BIND, fd.get_command());
+    
+    fd.set_command(SOCKS_CMD_UDP_ASSOCIATE);
+    CHECK_EQUAL(SOCKS_CMD_UDP_ASSOCIATE, fd.get_command());
+}
+
+// Test all address types
+TEST(SocksFlowDataBoundaryTests, test_all_address_types)
+{
+    SocksFlowData fd;
+    
+    fd.set_address_type(SOCKS_ATYP_IPV4);
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, fd.get_address_type());
+    
+    fd.set_address_type(SOCKS_ATYP_DOMAIN);
+    CHECK_EQUAL(SOCKS_ATYP_DOMAIN, fd.get_address_type());
+    
+    fd.set_address_type(SOCKS_ATYP_IPV6);
+    CHECK_EQUAL(SOCKS_ATYP_IPV6, fd.get_address_type());
+    
+    fd.set_bind_address_type(SOCKS_ATYP_IPV4);
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, fd.get_bind_address_type());
+}
+
+// Test all auth methods
+TEST(SocksFlowDataBoundaryTests, test_all_auth_methods)
+{
+    SocksFlowData fd;
+    
+    fd.set_auth_method(SOCKS5_AUTH_NONE);
+    CHECK_EQUAL(SOCKS5_AUTH_NONE, fd.get_auth_method());
+    
+    fd.set_auth_method(SOCKS5_AUTH_GSSAPI);
+    CHECK_EQUAL(SOCKS5_AUTH_GSSAPI, fd.get_auth_method());
+    
+    fd.set_auth_method(SOCKS5_AUTH_USERNAME_PASSWORD);
+    CHECK_EQUAL(SOCKS5_AUTH_USERNAME_PASSWORD, fd.get_auth_method());
+    
+    fd.set_auth_method(SOCKS5_AUTH_NO_ACCEPTABLE);
+    CHECK_EQUAL(SOCKS5_AUTH_NO_ACCEPTABLE, fd.get_auth_method());
+}
+
+// Test all reply codes
+TEST(SocksFlowDataBoundaryTests, test_all_reply_codes)
+{
+    SocksFlowData fd;
+    
+    const SocksReplyCode codes[] = {
+        SOCKS4_REP_GRANTED,
+        SOCKS4_REP_REJECTED,
+        SOCKS4_REP_NO_IDENTD,
+        SOCKS4_REP_IDENTD_FAILED,
+        SOCKS5_REP_SUCCESS,
+        SOCKS5_REP_GENERAL_FAILURE,
+        SOCKS5_REP_NOT_ALLOWED,
+        SOCKS5_REP_NETWORK_UNREACHABLE,
+        SOCKS5_REP_HOST_UNREACHABLE,
+        SOCKS5_REP_CONNECTION_REFUSED,
+        SOCKS5_REP_TTL_EXPIRED,
+        SOCKS5_REP_COMMAND_NOT_SUPPORTED,
+        SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED
+    };
+    
+    for (auto code : codes)
+    {
+        fd.set_last_error(code);
+        CHECK_EQUAL(code, fd.get_last_error());
+    }
+}
+
+// Test port boundary values
+TEST(SocksFlowDataBoundaryTests, test_port_boundaries)
+{
+    SocksFlowData fd;
+    
+    // Minimum port
+    fd.set_target_port(0);
+    CHECK_EQUAL(0, fd.get_target_port());
+    
+    // Maximum port
+    fd.set_target_port(65535);
+    CHECK_EQUAL(65535, fd.get_target_port());
+    
+    // Common ports
+    const uint16_t common_ports[] = {21, 22, 23, 25, 80, 443, 1080, 3128, 8080};
+    for (auto port : common_ports)
+    {
+        fd.set_target_port(port);
+        CHECK_EQUAL(port, fd.get_target_port());
+    }
+    
+    // Bind port
+    fd.set_bind_port(0);
+    CHECK_EQUAL(0, fd.get_bind_port());
+    
+    fd.set_bind_port(65535);
+    CHECK_EQUAL(65535, fd.get_bind_port());
+}
+
+// Test empty and null string handling
+TEST(SocksFlowDataBoundaryTests, test_empty_strings)
+{
+    SocksFlowData fd;
+    
+    // Empty target address
+    fd.set_target_address("");
+    CHECK_TRUE(fd.get_target_address().empty());
+    
+    // Empty bind address
+    fd.set_bind_address("");
+    CHECK_TRUE(fd.get_bind_address().empty());
+    
+    // Empty userid
+    fd.set_userid("");
+    CHECK_TRUE(fd.get_userid().empty());
+    
+    // Non-empty strings
+    fd.set_target_address("example.com");
+    STRCMP_EQUAL("example.com", fd.get_target_address().c_str());
+    
+    fd.set_bind_address("0.0.0.0");
+    STRCMP_EQUAL("0.0.0.0", fd.get_bind_address().c_str());
+    
+    fd.set_userid("testuser");
+    STRCMP_EQUAL("testuser", fd.get_userid().c_str());
+}
+
+// Test very long strings
+TEST(SocksFlowDataBoundaryTests, test_long_strings)
+{
+    SocksFlowData fd;
+    
+    // 253 character domain (RFC 1035 max)
+    std::string long_domain(253, 'a');
+    fd.set_target_address(long_domain);
+    CHECK_EQUAL(253, fd.get_target_address().length());
+    
+    // 255 character userid (SOCKS4 max)
+    std::string long_userid(255, 'u');
+    fd.set_userid(long_userid);
+    CHECK_EQUAL(255, fd.get_userid().length());
+}
+
+// Test IPv4 address handling
+TEST(SocksFlowDataBoundaryTests, test_ipv4_addresses)
+{
+    SocksFlowData fd;
+    
+    // Null initially
+    CHECK_TRUE(fd.get_target_ip() == nullptr);
+    
+    // Set valid IPv4
+    SfIp ip;
+    ip.set("10.0.0.1");
+    fd.set_target_ip(ip);
+    
+    // Verify it was set
+    auto target_ip = fd.get_target_ip();
+    if (target_ip)
+    {
+        CHECK_TRUE(target_ip->is_ip4());
+    }
+}
+
+// Test IPv6 address handling
+TEST(SocksFlowDataBoundaryTests, test_ipv6_addresses)
+{
+    SocksFlowData fd;
+    
+    // Set valid IPv6
+    SfIp ip;
+    ip.set("2001:db8::1");
+    fd.set_target_ip(ip);
+    
+    // Verify it was set
+    auto target_ip = fd.get_target_ip();
+    if (target_ip)
+    {
+        CHECK_TRUE(target_ip->is_ip6());
+    }
+}
+
+// Test counter overflow
+TEST(SocksFlowDataBoundaryTests, test_counter_overflow)
+{
+    SocksFlowData fd;
+    
+    // Increment many times
+    for (int i = 0; i < 1000; i++)
+    {
+        fd.increment_request_count();
+        fd.increment_response_count();
+    }
+    
+    CHECK_EQUAL(1000, fd.get_request_count());
+    CHECK_EQUAL(1000, fd.get_response_count());
+}
+
+// Test flag toggling
+TEST(SocksFlowDataBoundaryTests, test_flag_toggling)
+{
+    SocksFlowData fd;
+    
+    // Session counted
+    CHECK_FALSE(fd.is_session_counted());
+    fd.set_session_counted(true);
+    CHECK_TRUE(fd.is_session_counted());
+    fd.set_session_counted(false);
+    CHECK_FALSE(fd.is_session_counted());
+    
+    // Handoff pending
+    CHECK_FALSE(fd.is_handoff_pending());
+    fd.set_handoff_pending(true);
+    CHECK_TRUE(fd.is_handoff_pending());
+    fd.set_handoff_pending(false);
+    CHECK_FALSE(fd.is_handoff_pending());
+    
+    // Handoff completed
+    CHECK_FALSE(fd.is_handoff_completed());
+    fd.set_handoff_completed(true);
+    CHECK_TRUE(fd.is_handoff_completed());
+    fd.set_handoff_completed(false);
+    CHECK_FALSE(fd.is_handoff_completed());
+    
+    // SOCKS4a
+    CHECK_FALSE(fd.is_socks4a());
+    fd.set_socks4a(true);
+    CHECK_TRUE(fd.is_socks4a());
+    fd.set_socks4a(false);
+    CHECK_FALSE(fd.is_socks4a());
+}
+
+// Test version values
+TEST(SocksFlowDataBoundaryTests, test_version_values)
+{
+    SocksFlowData fd;
+    
+    fd.set_socks_version(SOCKS4_VERSION);
+    CHECK_EQUAL(SOCKS4_VERSION, fd.get_socks_version());
+    
+    fd.set_socks_version(SOCKS5_VERSION);
+    CHECK_EQUAL(SOCKS5_VERSION, fd.get_socks_version());
+    
+    // Invalid version (should still store it)
+    fd.set_socks_version(0x03);
+    CHECK_EQUAL(0x03, fd.get_socks_version());
+}
+
+// Test initiator values - IMPORTANT: initiator is write-once!
+// Per socks_flow_data.h:293-297, set_initiator() only works if initiator == UNKNOWN
+// This is by design - first detection wins, cannot be changed after
+TEST(SocksFlowDataBoundaryTests, test_initiator_write_once_behavior)
+{
+    SocksFlowData fd;
+    
+    // Initially UNKNOWN
+    CHECK_EQUAL(SOCKS_INITIATOR_UNKNOWN, fd.get_initiator());
+    CHECK_FALSE(fd.initiator_detected());
+    
+    // First set works
+    fd.set_initiator(SOCKS_INITIATOR_CLIENT);
+    CHECK_EQUAL(SOCKS_INITIATOR_CLIENT, fd.get_initiator());
+    CHECK_TRUE(fd.initiator_detected());
+    
+    // Second set is IGNORED (write-once behavior)
+    fd.set_initiator(SOCKS_INITIATOR_SERVER);
+    CHECK_EQUAL(SOCKS_INITIATOR_CLIENT, fd.get_initiator()); // Still CLIENT!
+    
+    // Test with fresh object - set to SERVER
+    SocksFlowData fd2;
+    fd2.set_initiator(SOCKS_INITIATOR_SERVER);
+    CHECK_EQUAL(SOCKS_INITIATOR_SERVER, fd2.get_initiator());
+    CHECK_TRUE(fd2.initiator_detected());
+}
+
+// Test direction values
+TEST(SocksFlowDataBoundaryTests, test_direction_values)
+{
+    SocksFlowData fd;
+    
+    fd.set_direction(SOCKS_DIR_CLIENT_TO_SERVER);
+    CHECK_EQUAL(SOCKS_DIR_CLIENT_TO_SERVER, fd.get_direction());
+    
+    fd.set_direction(SOCKS_DIR_SERVER_TO_CLIENT);
+    CHECK_EQUAL(SOCKS_DIR_SERVER_TO_CLIENT, fd.get_direction());
+}
+
+// Test error state with all error codes
+TEST(SocksFlowDataBoundaryTests, test_error_state_with_codes)
+{
+    SocksFlowData fd;
+    
+    fd.set_state(SOCKS_STATE_ERROR);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, fd.get_state());
+    
+    // Test with each error code
+    fd.set_last_error(SOCKS5_REP_GENERAL_FAILURE);
+    CHECK_EQUAL(SOCKS5_REP_GENERAL_FAILURE, fd.get_last_error());
+}
+
+// Test UDP reassembly pointer
+
+// Test state machine: SOCKS4 flow
+TEST(SocksFlowDataBoundaryTests, test_socks4_state_flow)
+{
+    SocksFlowData fd;
+    
+    fd.set_socks_version(SOCKS4_VERSION);
+    fd.set_state(SOCKS_STATE_INIT);
+    fd.set_command(SOCKS_CMD_CONNECT);
+    fd.increment_request_count();
+    
+    fd.set_state(SOCKS_STATE_V4_CONNECT_RESPONSE);
+    fd.increment_response_count();
+    
+    fd.set_state(SOCKS_STATE_ESTABLISHED);
+    
+    CHECK_EQUAL(SOCKS4_VERSION, fd.get_socks_version());
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, fd.get_state());
+    CHECK_EQUAL(1, fd.get_request_count());
+    CHECK_EQUAL(1, fd.get_response_count());
+}
+
+// Test state machine: SOCKS5 with no auth
+TEST(SocksFlowDataBoundaryTests, test_socks5_no_auth_flow)
+{
+    SocksFlowData fd;
+    
+    fd.set_socks_version(SOCKS5_VERSION);
+    fd.set_state(SOCKS_STATE_V5_AUTH_NEGOTIATION);
+    fd.increment_request_count();
+    
+    fd.set_auth_method(SOCKS5_AUTH_NONE);
+    fd.increment_response_count();
+    
+    fd.set_state(SOCKS_STATE_V5_CONNECT_REQUEST);
+    fd.set_command(SOCKS_CMD_CONNECT);
+    fd.increment_request_count();
+    
+    fd.set_state(SOCKS_STATE_V5_CONNECT_RESPONSE);
+    fd.set_last_error(SOCKS5_REP_SUCCESS);
+    fd.increment_response_count();
+    
+    fd.set_state(SOCKS_STATE_ESTABLISHED);
+    
+    CHECK_EQUAL(SOCKS5_VERSION, fd.get_socks_version());
+    CHECK_EQUAL(SOCKS5_AUTH_NONE, fd.get_auth_method());
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, fd.get_state());
+    CHECK_EQUAL(2, fd.get_request_count());
+    CHECK_EQUAL(2, fd.get_response_count());
+}
+
+// Test state machine: SOCKS5 with username/password auth
+TEST(SocksFlowDataBoundaryTests, test_socks5_userpass_auth_flow)
+{
+    SocksFlowData fd;
+    
+    fd.set_socks_version(SOCKS5_VERSION);
+    fd.set_state(SOCKS_STATE_V5_AUTH_NEGOTIATION);
+    fd.increment_request_count();
+    
+    fd.set_auth_method(SOCKS5_AUTH_USERNAME_PASSWORD);
+    fd.increment_response_count();
+    
+    fd.set_state(SOCKS_STATE_V5_USERNAME_PASSWORD_AUTH);
+    fd.set_userid("testuser");
+    fd.increment_request_count();
+    fd.increment_response_count();
+    
+    fd.set_state(SOCKS_STATE_V5_CONNECT_REQUEST);
+    fd.set_command(SOCKS_CMD_CONNECT);
+    fd.increment_request_count();
+    
+    fd.set_state(SOCKS_STATE_V5_CONNECT_RESPONSE);
+    fd.set_last_error(SOCKS5_REP_SUCCESS);
+    fd.increment_response_count();
+    
+    fd.set_state(SOCKS_STATE_ESTABLISHED);
+    
+    CHECK_EQUAL(SOCKS5_AUTH_USERNAME_PASSWORD, fd.get_auth_method());
+    STRCMP_EQUAL("testuser", fd.get_userid().c_str());
+    CHECK_EQUAL(3, fd.get_request_count());
+    CHECK_EQUAL(3, fd.get_response_count());
+}
+
+// Test BIND command flow
+TEST(SocksFlowDataBoundaryTests, test_bind_command_flow)
+{
+    SocksFlowData fd;
+    
+    fd.set_command(SOCKS_CMD_BIND);
+    fd.set_bind_address("0.0.0.0");
+    fd.set_bind_port(0);
+    fd.set_bind_address_type(SOCKS_ATYP_IPV4);
+    
+    CHECK_EQUAL(SOCKS_CMD_BIND, fd.get_command());
+    STRCMP_EQUAL("0.0.0.0", fd.get_bind_address().c_str());
+    CHECK_EQUAL(0, fd.get_bind_port());
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, fd.get_bind_address_type());
+}
+
+// Test UDP ASSOCIATE command flow
+TEST(SocksFlowDataBoundaryTests, test_udp_associate_flow)
+{
+    SocksFlowData fd;
+    
+    fd.set_command(SOCKS_CMD_UDP_ASSOCIATE);
+    fd.set_bind_address("0.0.0.0");
+    fd.set_bind_port(1080);
+    fd.set_state(SOCKS_STATE_ESTABLISHED);
+    
+    CHECK_EQUAL(SOCKS_CMD_UDP_ASSOCIATE, fd.get_command());
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, fd.get_state());
+}
+
+//-------------------------------------------------------------------------
+// get_xtra_target_ip Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(GetXtraTargetIpTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - modified in tests
+    Flow* flow = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - modified in tests
+    SocksFlowData* flow_data = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - assigned in tests
+    // cppcheck-suppress unreadVariable ; false positive - used in tests
+    uint8_t* buf = nullptr;
+    // cppcheck-suppress unreadVariable ; false positive - used in tests
+    uint32_t len = 0;
+    // cppcheck-suppress unreadVariable ; false positive - used in tests
+    uint32_t type = 0;
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow = new Flow();
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+    }
+
+    void teardown() override
+    {
+        delete flow_data;
+        delete flow;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(GetXtraTargetIpTests, null_flow_returns_zero)
+{
+    int result = SocksInspector::get_xtra_target_ip(nullptr, &buf, &len, &type);
+    CHECK_EQUAL(0, result);
+}
+
+TEST(GetXtraTargetIpTests, no_flow_data_returns_zero)
+{
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+    CHECK_EQUAL(0, result);
+}
+
+TEST(GetXtraTargetIpTests, no_target_ip_returns_zero)
+{
+    flow->set_flow_data(flow_data);
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+    CHECK_EQUAL(0, result);
+}
+
+TEST(GetXtraTargetIpTests, unset_target_ip_returns_zero)
+{
+    flow->set_flow_data(flow_data);
+
+    flow_data->set_target_address("example.com");
+    flow_data->set_target_port(80);
+
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+    CHECK_EQUAL(0, result);
+}
+
+TEST(GetXtraTargetIpTests, ipv4_target_returns_success)
+{
+    flow->set_flow_data(flow_data);
+
+    SfIp target_ip;
+    target_ip.set("192.168.1.100");
+    flow_data->set_target_ip(target_ip);
+    flow_data->set_target_port(80);
+
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+    
+    CHECK_EQUAL(1, result);
+    CHECK(buf != nullptr);
+    CHECK_EQUAL(4, len);
+    CHECK_EQUAL(EVENT_INFO_XFF_IPV4, type);
+
+    CHECK_EQUAL(192, buf[0]);
+    CHECK_EQUAL(168, buf[1]);
+    CHECK_EQUAL(1, buf[2]);
+    CHECK_EQUAL(100, buf[3]);
+}
+
+TEST(GetXtraTargetIpTests, ipv6_target_returns_success)
+{
+    flow->set_flow_data(flow_data);
+
+    SfIp target_ip;
+    target_ip.set("2001:db8::1");
+    flow_data->set_target_ip(target_ip);
+    flow_data->set_target_port(443);
+    
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+    
+    CHECK_EQUAL(1, result);
+    CHECK(buf != nullptr);
+    CHECK_EQUAL(16, len);
+    CHECK_EQUAL(EVENT_INFO_XFF_IPV6, type);
+
+    CHECK_EQUAL(0x20, buf[0]);
+    CHECK_EQUAL(0x01, buf[1]);
+    CHECK_EQUAL(0x0d, buf[2]);
+    CHECK_EQUAL(0xb8, buf[3]);
+}
+
+TEST(GetXtraTargetIpTests, various_ipv4_addresses)
+{
+    flow->set_flow_data(flow_data);
+
+    SfIp target_ip;
+    target_ip.set("10.0.0.1");
+    flow_data->set_target_ip(target_ip);
+    flow_data->set_target_port(22);
+
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+
+    CHECK_EQUAL(1, result);
+    CHECK_EQUAL(4, len);
+    CHECK_EQUAL(EVENT_INFO_XFF_IPV4, type);
+    CHECK_EQUAL(10, buf[0]);
+    CHECK_EQUAL(0, buf[1]);
+    CHECK_EQUAL(0, buf[2]);
+    CHECK_EQUAL(1, buf[3]);
+}
+
+TEST(GetXtraTargetIpTests, ipv4_loopback)
+{
+    flow->set_flow_data(flow_data);
+
+    SfIp target_ip;
+    target_ip.set("127.0.0.1");
+    flow_data->set_target_ip(target_ip);
+    flow_data->set_target_port(8080);
+
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+
+    CHECK_EQUAL(1, result);
+    CHECK_EQUAL(4, len);
+    CHECK_EQUAL(EVENT_INFO_XFF_IPV4, type);
+    CHECK_EQUAL(127, buf[0]);
+    CHECK_EQUAL(0, buf[1]);
+    CHECK_EQUAL(0, buf[2]);
+    CHECK_EQUAL(1, buf[3]);
+}
+
+TEST(GetXtraTargetIpTests, ipv6_loopback)
+{
+    flow->set_flow_data(flow_data);
+
+    SfIp target_ip;
+    target_ip.set("::1");
+    flow_data->set_target_ip(target_ip);
+    flow_data->set_target_port(8080);
+
+    int result = SocksInspector::get_xtra_target_ip(flow, &buf, &len, &type);
+
+    CHECK_EQUAL(1, result);
+    CHECK_EQUAL(16, len);
+    CHECK_EQUAL(EVENT_INFO_XFF_IPV6, type);
+
+    for (int i = 0; i < 15; i++)
+        CHECK_EQUAL(0, buf[i]);
+    CHECK_EQUAL(1, buf[15]);
+}
+
+//-------------------------------------------------------------------------
+// detect_protocol_initiator Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(DetectProtocolInitiatorTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+    Packet* packet = nullptr;
+    uint8_t packet_data[256];
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+        packet = new Packet();
+        packet->data = packet_data;
+        packet->packet_flags = 0;
+        packet->proto_bits = 0;
+        packet->dsize = 0;
+        memset(packet_data, 0, sizeof(packet_data));
+    }
+
+    void teardown() override
+    {
+        delete packet;
+        delete flow_data;
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(DetectProtocolInitiatorTests, null_data_returns_early)
+{
+    packet->data = nullptr;
+    packet->dsize = 10;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    CHECK_FALSE(flow_data->initiator_detected());
+}
+
+TEST(DetectProtocolInitiatorTests, packet_too_short)
+{
+    packet->dsize = 1;  // Less than 2
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    CHECK_FALSE(flow_data->initiator_detected());
+}
+
+TEST(DetectProtocolInitiatorTests, client_socks4_detected)
+{
+    packet->packet_flags |= PKT_FROM_CLIENT;
+    
+    packet_data[0] = SOCKS4_VERSION;  // 0x04
+    packet_data[1] = 0x01;
+    packet->dsize = 10;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    
+    CHECK_EQUAL(SOCKS_INITIATOR_CLIENT, flow_data->get_initiator());
+}
+
+TEST(DetectProtocolInitiatorTests, client_socks5_detected)
+{
+    packet->packet_flags |= PKT_FROM_CLIENT;
+    
+    packet_data[0] = SOCKS5_VERSION;  // 0x05
+    packet_data[1] = 0x01;
+    packet->dsize = 10;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    
+    CHECK_EQUAL(SOCKS_INITIATOR_CLIENT, flow_data->get_initiator());
+}
+
+// Client with unknown version
+TEST(DetectProtocolInitiatorTests, client_unknown_version)
+{
+    packet->packet_flags |= PKT_FROM_CLIENT;
+    
+    packet_data[0] = 0x03;  // Unknown version
+    packet_data[1] = 0x01;
+    packet->dsize = 10;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    CHECK_FALSE(flow_data->initiator_detected());
+}
+
+TEST(DetectProtocolInitiatorTests, server_socks5_detected)
+{
+    packet->packet_flags &= ~PKT_FROM_CLIENT;  // From server
+    
+    packet_data[0] = SOCKS5_VERSION;  // 0x05
+    packet_data[1] = 0x00;
+    packet->dsize = 2;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    
+    CHECK_EQUAL(SOCKS_INITIATOR_SERVER, flow_data->get_initiator());
+    CHECK_EQUAL(SOCKS5_VERSION, flow_data->get_socks_version());
+}
+
+TEST(DetectProtocolInitiatorTests, server_socks4_response_not_supported)
+{
+    // Reverse SOCKS4 (server-initiated) is not supported.
+    // SOCKS4 responses start with 0x00 which is ambiguous, and the reverse
+    // processing path only handles SOCKS5.
+    packet->packet_flags &= ~PKT_FROM_CLIENT;  // From server
+    
+    packet_data[0] = SOCKS4_RESPONSE_VERSION;  // 0x00
+    packet_data[1] = 0x5A;  // Request granted
+    packet->dsize = 8;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    
+    // Initiator should NOT be set - reverse SOCKS4 is unsupported
+    CHECK_FALSE(flow_data->initiator_detected());
+}
+
+TEST(DetectProtocolInitiatorTests, server_unknown_version)
+{
+    packet->packet_flags &= ~PKT_FROM_CLIENT;  // From server
+    
+    packet_data[0] = 0x03;  // Unknown version
+    packet_data[1] = 0x00;
+    packet->dsize = 10;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    CHECK_FALSE(flow_data->initiator_detected());
+}
+
+TEST(DetectProtocolInitiatorTests, already_detected_not_changed)
+{
+    flow_data->set_initiator(SOCKS_INITIATOR_CLIENT);
+    
+    packet->packet_flags &= ~PKT_FROM_CLIENT;  // From server
+    packet_data[0] = SOCKS5_VERSION;
+    packet->dsize = 2;
+    
+    inspector->detect_protocol_initiator(packet, flow_data);
+    
+    // Should still be CLIENT (write-once behavior)
+    CHECK_EQUAL(SOCKS_INITIATOR_CLIENT, flow_data->get_initiator());
+}
+
+//-------------------------------------------------------------------------
+// process_reverse_server_data Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(ProcessReverseServerDataTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+    Packet* packet = nullptr;
+    uint8_t packet_data[256];
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+        packet = new Packet();
+        packet->data = packet_data;
+        memset(packet_data, 0, sizeof(packet_data));
+    }
+
+    void teardown() override
+    {
+        delete packet;
+        delete flow_data;
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(ProcessReverseServerDataTests, established_state_processes_tunneled_data)
+{
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_command(SOCKS_CMD_BIND);
+    
+    packet_data[0] = 0x05;
+    packet_data[1] = 0x00;
+    packet->dsize = 10;
+
+    inspector->process_reverse_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, error_state_handled)
+{
+    flow_data->set_state(SOCKS_STATE_ERROR);
+
+    packet_data[0] = 0x05;
+    packet->dsize = 2;
+
+    inspector->process_reverse_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, packet_too_short)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    
+    packet_data[0] = 0x05;
+    packet->dsize = 1;  // Too short
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, socks5_auth_response_no_auth)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // No auth
+    packet->dsize = 2;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    // Server sends auth response, transitions to AUTH_RESPONSE state
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, socks5_auth_response_error)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0xFF;  // No acceptable methods
+    packet->dsize = 2;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    // Server sends error auth response, transitions to ERROR state
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, ignores_connect_response_from_client)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // Success
+    packet_data[2] = 0x00;  // Reserved
+    packet_data[3] = 0x01;  // IPv4
+    packet_data[4] = 192;   // IP: 192.168.1.1
+    packet_data[5] = 168;
+    packet_data[6] = 1;
+    packet_data[7] = 1;
+    packet_data[8] = 0x04;  // Port: 1080
+    packet_data[9] = 0x38;
+    packet->dsize = 10;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    // Connect response should be ignored by server-side processing (client sends this)
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, ignores_connect_response_failure_from_client)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x01;  // General failure
+    packet_data[2] = 0x00;  // Reserved
+    packet_data[3] = 0x01;  // IPv4
+    packet_data[4] = 0;
+    packet_data[5] = 0;
+    packet_data[6] = 0;
+    packet_data[7] = 0;
+    packet_data[8] = 0;
+    packet_data[9] = 0;
+    packet->dsize = 10;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    // Connect response should be ignored by server-side processing (client sends this)
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, ignores_bind_response_from_client)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+    flow_data->set_command(SOCKS_CMD_BIND);  // Not CONNECT
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // Success
+    packet_data[2] = 0x00;  // Reserved
+    packet_data[3] = 0x01;  // IPv4
+    packet_data[4] = 10;
+    packet_data[5] = 0;
+    packet_data[6] = 0;
+    packet_data[7] = 1;
+    packet_data[8] = 0x00;
+    packet_data[9] = 0x50;
+    packet->dsize = 10;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    // Bind response should be ignored by server-side processing (client sends this)
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, socks5_invalid_reserved_byte)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;
+    packet_data[2] = 0xFF;  // Invalid reserved byte (should be 0x00)
+    packet_data[3] = 0x01;
+    packet->dsize = 10;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, socks4_response_success)
+{
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_REQUEST);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_socks_version(SOCKS4_VERSION);
+    
+    packet_data[0] = 0x00;  // SOCKS4 response version (0x00, NOT 0x04!)
+    packet_data[1] = 0x5A;  // Request granted
+    packet_data[2] = 0x04;  // Port high byte
+    packet_data[3] = 0x38;  // Port low byte (1080)
+    packet_data[4] = 192;   // IP: 192.168.1.1
+    packet_data[5] = 168;
+    packet_data[6] = 1;
+    packet_data[7] = 1;
+    packet->dsize = 8;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    
+    // Server processes SOCKS4, stays in CONNECT_REQUEST state
+    CHECK_EQUAL(SOCKS_STATE_V4_CONNECT_REQUEST, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, socks4_response_too_short)
+{
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_REQUEST);
+    
+    packet_data[0] = 0x00;  // Correct SOCKS4 response version
+    packet_data[1] = 0x5A;
+    packet->dsize = 7;  // Too short
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_V4_CONNECT_REQUEST, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, socks4_bind_response_no_handoff)
+{
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_REQUEST);
+    flow_data->set_command(SOCKS_CMD_BIND);  // BIND, not CONNECT
+    flow_data->set_socks_version(SOCKS4_VERSION);
+    
+    packet_data[0] = 0x00;  // SOCKS4 response version
+    packet_data[1] = 0x5A;  // Request granted
+    packet_data[2] = 0x04;
+    packet_data[3] = 0x38;
+    packet_data[4] = 10;
+    packet_data[5] = 0;
+    packet_data[6] = 0;
+    packet_data[7] = 1;
+    packet->dsize = 8;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    
+    // Server processes SOCKS4, stays in CONNECT_REQUEST state
+    CHECK_EQUAL(SOCKS_STATE_V4_CONNECT_REQUEST, flow_data->get_state());
+}
+
+TEST(ProcessReverseServerDataTests, unknown_version)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    
+    packet_data[0] = 0x03;  // Unknown version
+    packet_data[1] = 0x00;
+    packet->dsize = 10;
+    
+    inspector->process_reverse_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION, flow_data->get_state());
+}
+
+//-------------------------------------------------------------------------
+// process_reverse_client_data Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(ProcessReverseClientDataTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+    Packet* packet = nullptr;
+    uint8_t packet_data[256];
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+        packet = new Packet();
+        packet->data = packet_data;
+        memset(packet_data, 0, sizeof(packet_data));
+    }
+
+    void teardown() override
+    {
+        delete packet;
+        delete flow_data;
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(ProcessReverseClientDataTests, established_state_processes_tunneled_data)
+{
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_command(SOCKS_CMD_BIND);
+    
+    packet_data[0] = 0x05;
+    packet_data[1] = 0x01;
+    packet->dsize = 10;
+
+    inspector->process_reverse_client_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, error_state_handled)
+{
+    flow_data->set_state(SOCKS_STATE_ERROR);
+
+    packet_data[0] = 0x05;
+    packet->dsize = 2;
+
+    inspector->process_reverse_client_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, packet_too_short)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+    
+    packet_data[0] = 0x05;
+    packet->dsize = 1;  // Too short
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, wrong_version)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+    
+    packet_data[0] = 0x04;  // SOCKS4, not SOCKS5
+    packet_data[1] = 0x01;
+    packet->dsize = 10;
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, username_password_auth_in_auth_response_state)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+    flow_data->set_auth_method(SOCKS5_AUTH_USERNAME_PASSWORD);
+
+    packet_data[0] = 0x01;  // Auth version
+    packet_data[1] = 0x04;  // Username length
+    packet_data[2] = 'u';
+    packet_data[3] = 's';
+    packet_data[4] = 'e';
+    packet_data[5] = 'r';
+    packet_data[6] = 0x04;  // Password length
+    packet_data[7] = 'p';
+    packet_data[8] = 'a';
+    packet_data[9] = 's';
+    packet_data[10] = 's';
+    packet->dsize = 11;
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, ignores_auth_negotiation_from_server)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+    flow_data->set_auth_method(SOCKS5_AUTH_NONE);
+
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x02;  // 2 methods
+    packet_data[2] = 0x00;  // No auth
+    packet_data[3] = 0x02;  // Username/password
+    packet->dsize = 4;
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    // Auth negotiation should be ignored by client-side processing (server sends this)
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, connect_response_success)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    flow_data->set_auth_method(SOCKS5_AUTH_NONE);
+
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // Reply code: Success
+    packet_data[2] = 0x00;  // Reserved (must be 0)
+    packet_data[3] = 0x01;  // IPv4
+    packet_data[4] = 192;   // IP: 192.168.1.1
+    packet_data[5] = 168;
+    packet_data[6] = 1;
+    packet_data[7] = 1;
+    packet_data[8] = 0x00;  // Port: 80
+    packet_data[9] = 0x50;
+    packet->dsize = 10;
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    // Client sends connect response, transitions to ESTABLISHED for CONNECT
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+    CHECK_TRUE(flow_data->is_handoff_pending());
+}
+
+TEST(ProcessReverseClientDataTests, bind_response_success)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // Reply code: Success
+    packet_data[2] = 0x00;  // Reserved
+    packet_data[3] = 0x01;  // IPv4
+    packet_data[4] = 10;    // IP: 10.0.0.1
+    packet_data[5] = 0;
+    packet_data[6] = 0;
+    packet_data[7] = 1;
+    packet_data[8] = 0x1F;  // Port: 8080
+    packet_data[9] = 0x90;
+    packet->dsize = 10;
+    
+    flow_data->set_command(SOCKS_CMD_BIND);  // Set command to BIND
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    // Client sends bind response, transitions to ESTABLISHED for BIND (no handoff)
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+    CHECK_FALSE(flow_data->is_handoff_pending());
+}
+
+TEST(ProcessReverseClientDataTests, udp_associate_response_success)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // Reply code: Success
+    packet_data[2] = 0x00;  // Reserved
+    packet_data[3] = 0x01;  // IPv4
+    packet_data[4] = 0;     // IP: 0.0.0.0
+    packet_data[5] = 0;
+    packet_data[6] = 0;
+    packet_data[7] = 0;
+    packet_data[8] = 0x04;  // Port: 1080
+    packet_data[9] = 0x38;
+    packet->dsize = 10;
+    
+    flow_data->set_command(SOCKS_CMD_UDP_ASSOCIATE);  // Set command to UDP_ASSOCIATE
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    // Client sends UDP associate response, transitions to ESTABLISHED (no handoff)
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+    CHECK_FALSE(flow_data->is_handoff_pending());
+}
+
+TEST(ProcessReverseClientDataTests, username_password_auth_fallback)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    flow_data->set_auth_method(SOCKS5_AUTH_USERNAME_PASSWORD);
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // 0 methods (invalid for auth negotiation)
+    packet->dsize = 2;
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    // Invalid auth negotiation, stays in AUTH_RESPONSE state
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, ignores_auth_negotiation_single_method_from_server)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x01;  // 1 method
+    packet_data[2] = 0x00;  // No auth
+    packet->dsize = 3;
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    // Client sends auth response, stays in AUTH_RESPONSE state
+    CHECK_EQUAL(SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE, flow_data->get_state());
+}
+
+TEST(ProcessReverseClientDataTests, connect_response_with_domain)
+{
+    flow_data->set_state(SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    flow_data->set_command(SOCKS_CMD_CONNECT);  // Set command to CONNECT
+    
+    packet_data[0] = 0x05;  // SOCKS5
+    packet_data[1] = 0x00;  // Reply code: Success
+    packet_data[2] = 0x00;  // Reserved
+    packet_data[3] = 0x03;  // Domain name
+    packet_data[4] = 0x0B;  // Length: 11
+    memcpy(&packet_data[5], "example.com", 11);
+    packet_data[16] = 0x00; // Port: 443
+    packet_data[17] = 0xBB;
+    packet->dsize = 18;
+    
+    inspector->process_reverse_client_data(packet, flow_data);
+    
+    // Client sends connect response with domain, transitions to ESTABLISHED
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+    CHECK_TRUE(flow_data->is_handoff_pending());
+}
+
+//-------------------------------------------------------------------------
+// process_client_data and process_server_data State Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(ProcessDataStateTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+    Packet* packet = nullptr;
+    uint8_t packet_data[256];
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+        packet = new Packet();
+        packet->data = packet_data;
+        memset(packet_data, 0, sizeof(packet_data));
+        packet->dsize = 10;
+    }
+
+    void teardown() override
+    {
+        delete packet;
+        delete flow_data;
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(ProcessDataStateTests, client_established_state)
+{
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    
+    inspector->process_client_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+}
+
+TEST(ProcessDataStateTests, server_established_state)
+{
+    flow_data->set_state(SOCKS_STATE_ESTABLISHED);
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    
+    inspector->process_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ESTABLISHED, flow_data->get_state());
+}
+
+TEST(ProcessDataStateTests, client_error_state)
+{
+    flow_data->set_state(SOCKS_STATE_ERROR);
+    packet_data[0] = 0x05;
+    
+    inspector->process_client_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+TEST(ProcessDataStateTests, server_error_state)
+{
+    flow_data->set_state(SOCKS_STATE_ERROR);
+    packet_data[0] = 0x05;
+    
+    inspector->process_server_data(packet, flow_data);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+//-------------------------------------------------------------------------
+// validate_socks5_request_header Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(ValidateSocks5RequestHeaderTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    uint8_t data[256];
+
+    void setup() override
+    {
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        memset(data, 0, sizeof(data));
+    }
+
+    void teardown() override
+    {
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(ValidateSocks5RequestHeaderTests, valid_request_returns_true)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0x01;  // CONNECT
+    req.reserved = 0x00;
+    req.address_type = 0x01;  // IPv4
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_TRUE(result);
+}
+
+TEST(ValidateSocks5RequestHeaderTests, invalid_version_returns_false)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x04;  // Invalid - should be 0x05
+    req.command = 0x01;
+    req.reserved = 0x00;
+    req.address_type = 0x01;
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_FALSE(result);
+}
+
+TEST(ValidateSocks5RequestHeaderTests, invalid_command_returns_false)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0xFF;  // Invalid command
+    req.reserved = 0x00;
+    req.address_type = 0x01;
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_FALSE(result);
+}
+
+TEST(ValidateSocks5RequestHeaderTests, nonzero_reserved_still_validates)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0x01;
+    req.reserved = 0x01;  // Non-zero reserved (protocol violation but not fatal)
+    req.address_type = 0x01;
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_TRUE(result);  // Should still return true (warning only)
+}
+
+TEST(ValidateSocks5RequestHeaderTests, invalid_address_type_returns_false)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0x01;
+    req.reserved = 0x00;
+    req.address_type = 0xFF;  // Invalid address type
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_FALSE(result);
+}
+
+TEST(ValidateSocks5RequestHeaderTests, valid_bind_command)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0x02;  // BIND
+    req.reserved = 0x00;
+    req.address_type = 0x01;
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_TRUE(result);
+}
+
+TEST(ValidateSocks5RequestHeaderTests, valid_udp_associate_command)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0x03;  // UDP_ASSOCIATE
+    req.reserved = 0x00;
+    req.address_type = 0x01;
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_TRUE(result);
+}
+
+TEST(ValidateSocks5RequestHeaderTests, valid_domain_address_type)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0x01;
+    req.reserved = 0x00;
+    req.address_type = 0x03;  // Domain name
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_TRUE(result);
+}
+
+TEST(ValidateSocks5RequestHeaderTests, valid_ipv6_address_type)
+{
+    Socks5ConnectRequest req;
+    req.version = 0x05;
+    req.command = 0x01;
+    req.reserved = 0x00;
+    req.address_type = 0x04;  // IPv6
+    
+    bool result = inspector->validate_socks5_request_header(&req);
+    CHECK_TRUE(result);
+}
+
+//-------------------------------------------------------------------------
+// parse_socks5_auth_response Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(ParseSocks5AuthResponseTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+    uint8_t data[256];
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+        memset(data, 0, sizeof(data));
+    }
+
+    void teardown() override
+    {
+        delete flow_data;
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(ParseSocks5AuthResponseTests, insufficient_length_returns_false)
+{
+    data[0] = 0x05;
+    bool result = inspector->parse_socks5_auth_response(data, 1, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks5AuthResponseTests, no_acceptable_methods_triggers_auth_failure)
+{
+    data[0] = 0x05;
+    data[1] = 0xFF;  // SOCKS5_AUTH_NO_ACCEPTABLE
+    
+    bool result = inspector->parse_socks5_auth_response(data, 2, flow_data);
+    
+    CHECK_FALSE(result);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+TEST(ParseSocks5AuthResponseTests, session_counted_when_server_initiator)
+{
+    flow_data->set_initiator(SOCKS_INITIATOR_SERVER);
+    flow_data->set_session_counted(false);
+    
+    data[0] = 0x05;
+    data[1] = 0x00;  // SOCKS5_AUTH_NONE
+    
+    bool result = inspector->parse_socks5_auth_response(data, 2, flow_data);
+    
+    CHECK_TRUE(result);
+    CHECK_TRUE(flow_data->is_session_counted());
+}
+
+TEST(ParseSocks5AuthResponseTests, unsupported_auth_method_skips_to_connect_request)
+{
+    data[0] = 0x05;
+    data[1] = 0x01;  // GSSAPI (unsupported)
+    
+    bool result = inspector->parse_socks5_auth_response(data, 2, flow_data);
+    
+    CHECK_TRUE(result);
+    // skips auth phase and waits for CONNECT/BIND/UDP_ASSOCIATE
+    CHECK_EQUAL(SOCKS_STATE_V5_CONNECT_REQUEST, flow_data->get_state());
+    CHECK_FALSE(flow_data->is_handoff_pending());
+}
+
+TEST(ParseSocks5AuthResponseTests, private_auth_method_skips_to_connect_request)
+{
+    data[0] = 0x05;
+    data[1] = 0x80;  // Private method (0x80-0xFE)
+    
+    bool result = inspector->parse_socks5_auth_response(data, 2, flow_data);
+    
+    CHECK_TRUE(result);
+    // skips auth phase and waits for CONNECT/BIND/UDP_ASSOCIATE
+    CHECK_EQUAL(SOCKS_STATE_V5_CONNECT_REQUEST, flow_data->get_state());
+    CHECK_FALSE(flow_data->is_handoff_pending());
+}
+
+//-------------------------------------------------------------------------
+// parse_socks4_response Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(ParseSocks4ResponseTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+    uint8_t data[256];
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+        memset(data, 0, sizeof(data));
+    }
+
+    void teardown() override
+    {
+        delete flow_data;
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(ParseSocks4ResponseTests, null_data_returns_false)
+{
+    bool result = inspector->parse_socks4_response(nullptr, 10, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4ResponseTests, insufficient_length_returns_false)
+{
+    data[0] = 0x00;
+    bool result = inspector->parse_socks4_response(data, 7, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4ResponseTests, invalid_version_returns_false)
+{
+    data[0] = 0x04;  // Should be 0x00
+    data[1] = 0x5A;  // GRANTED
+    bool result = inspector->parse_socks4_response(data, 8, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4ResponseTests, bind_first_response_transitions_to_bind_second)
+{
+    flow_data->set_command(SOCKS_CMD_BIND);
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_RESPONSE);
+    
+    data[0] = 0x00;  // Version
+    data[1] = 0x5A;  // GRANTED
+    data[2] = 0x00;  // Port high
+    data[3] = 0x50;  // Port low (80)
+    data[4] = 192;   // IP
+    data[5] = 168;
+    data[6] = 1;
+    data[7] = 1;
+    
+    bool result = inspector->parse_socks4_response(data, 8, flow_data);
+    
+    CHECK_TRUE(result);
+    CHECK_EQUAL(SOCKS_STATE_V4_BIND_SECOND_RESPONSE, flow_data->get_state());
+}
+
+TEST(ParseSocks4ResponseTests, connection_rejected_sets_error_state)
+{
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target_address("192.168.1.100");
+    flow_data->set_target_port(80);
+    
+    data[0] = 0x00;  // Version
+    data[1] = 0x5B;  // REJECTED (not 0x5A)
+    data[2] = 0x00;
+    data[3] = 0x00;
+    data[4] = 0;
+    data[5] = 0;
+    data[6] = 0;
+    data[7] = 0;
+    
+    bool result = inspector->parse_socks4_response(data, 8, flow_data);
+    
+    CHECK_FALSE(result);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+TEST(ParseSocks4ResponseTests, rejected_with_different_status_codes)
+{
+    flow_data->set_command(SOCKS_CMD_CONNECT);
+    flow_data->set_target_address("10.0.0.1");
+    flow_data->set_target_port(22);
+    
+    data[0] = 0x00;
+    data[1] = 0x5C;  // Request rejected - identd not reachable
+    
+    bool result = inspector->parse_socks4_response(data, 8, flow_data);
+    
+    CHECK_FALSE(result);
+    CHECK_EQUAL(SOCKS_STATE_ERROR, flow_data->get_state());
+}
+
+//-------------------------------------------------------------------------
+// parse_socks4_request Tests - Full Coverage
+//-------------------------------------------------------------------------
+
+TEST_GROUP(ParseSocks4RequestTests)
+{
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksInspectorTestHelper* inspector = nullptr;
+    // cppcheck-suppress constVariablePointer ; false positive - reassigned in setup()
+    SocksFlowData* flow_data = nullptr;
+    uint8_t data[512];  // Increased size to accommodate userid_too_long test (needs 265 bytes)
+
+    void setup() override
+    {
+        SocksFlowData::init();
+        SocksModule module;
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        inspector = new SocksInspectorTestHelper(&module);
+        // cppcheck-suppress unreadVariable ; false positive - used in tests
+        flow_data = new SocksFlowData();
+        memset(data, 0, sizeof(data));
+    }
+
+    void teardown() override
+    {
+        delete flow_data;
+        delete inspector;
+        clear_stored_flow_data();
+    }
+};
+
+TEST(ParseSocks4RequestTests, null_data_returns_false)
+{
+    bool result = inspector->parse_socks4_request(nullptr, 10, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4RequestTests, insufficient_length_returns_false)
+{
+    data[0] = 0x04;
+    bool result = inspector->parse_socks4_request(data, 8, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4RequestTests, invalid_version_returns_false)
+{
+    data[0] = 0x05;  // Should be 0x04
+    data[1] = 0x01;
+    bool result = inspector->parse_socks4_request(data, 10, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4RequestTests, invalid_command_returns_false)
+{
+    data[0] = 0x04;
+    data[1] = 0xFF;  // Invalid command
+    bool result = inspector->parse_socks4_request(data, 10, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4RequestTests, missing_userid_null_terminator_returns_false)
+{
+    data[0] = 0x04;
+    data[1] = 0x01;  // CONNECT
+    data[2] = 0x00;  // Port
+    data[3] = 0x50;
+    data[4] = 192;   // IP
+    data[5] = 168;
+    data[6] = 1;
+    data[7] = 1;
+    data[8] = 'u';   // Userid starts but no null terminator within length
+    
+    bool result = inspector->parse_socks4_request(data, 9, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4RequestTests, userid_too_long_returns_false)
+{
+    data[0] = 0x04;
+    data[1] = 0x01;
+    data[2] = 0x00;
+    data[3] = 0x50;
+    data[4] = 192;
+    data[5] = 168;
+    data[6] = 1;
+    data[7] = 1;
+    // Fill with 256 bytes (too long)
+    memset(&data[8], 'a', 256);
+    data[264] = 0;  // Null terminator
+    
+    bool result = inspector->parse_socks4_request(data, 265, flow_data);
+    CHECK_FALSE(result);
+}
+
+TEST(ParseSocks4RequestTests, minimal_valid_request_with_empty_userid)
+{
+    data[0] = 0x04;
+    data[1] = 0x01;
+    data[2] = 0x00;
+    data[3] = 0x50;
+    data[4] = 192;
+    data[5] = 168;
+    data[6] = 1;
+    data[7] = 1;
+    data[8] = 0;  // Null terminator for empty userid
+    
+    bool result = inspector->parse_socks4_request(data, 9, flow_data);
+    CHECK_TRUE(result);  // This is a valid SOCKS4 request with empty userid
+    CHECK_EQUAL(SOCKS4_VERSION, flow_data->get_socks_version());
+    CHECK_EQUAL(SOCKS_CMD_CONNECT, flow_data->get_command());
+}
+
+TEST(ParseSocks4RequestTests, socks4_ipv4_request_success)
+{
+    data[0] = 0x04;  // Version
+    data[1] = 0x01;  // CONNECT
+    data[2] = 0x00;  // Port high
+    data[3] = 0x50;  // Port low (80)
+    data[4] = 192;   // IP: 192.168.1.100
+    data[5] = 168;
+    data[6] = 1;
+    data[7] = 100;
+    data[8] = 'u';   // Userid
+    data[9] = 's';
+    data[10] = 'e';
+    data[11] = 'r';
+    data[12] = 0;    // Null terminator
+    
+    bool result = inspector->parse_socks4_request(data, 13, flow_data);
+    
+    CHECK_TRUE(result);
+    CHECK_EQUAL(SOCKS4_VERSION, flow_data->get_socks_version());
+    CHECK_EQUAL(SOCKS_CMD_CONNECT, flow_data->get_command());
+    CHECK_EQUAL(80, flow_data->get_target_port());
+    CHECK_EQUAL(SOCKS_ATYP_IPV4, flow_data->get_address_type());
+    CHECK_FALSE(flow_data->is_socks4a());
+}
+
+int main(int argc, char** argv)
+{
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
diff --git a/src/service_inspectors/socks/test/socks_splitter_negative_test.cc b/src/service_inspectors/socks/test/socks_splitter_negative_test.cc
new file mode 100644 (file)
index 0000000..5b70d1a
--- /dev/null
@@ -0,0 +1,556 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// socks_splitter_negative_test.cc author Raza Shafiq <rshafiq@cisco.com>
+// Negative test cases for SOCKS splitter to improve code coverage
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+#include "../socks_flow_data.h"
+
+// Access private methods for testing
+#define private public
+#include "../socks_splitter.h"
+#undef private
+
+#include "stream/stream_splitter.h"
+
+namespace snort
+{
+    class Flow;
+    class Packet;
+}
+
+// Forward declare SocksStats
+struct SocksStats
+{
+    uint64_t concurrent_sessions = 0;
+    uint64_t max_concurrent_sessions = 0;
+};
+
+using namespace snort;
+
+// Mock implementations - copied from socks_splitter_test.cc
+namespace snort
+{
+    class Packet
+    {
+    public:
+        Flow* flow = nullptr;
+        
+        Packet(bool) { flow = nullptr; }
+        ~Packet() = default;
+    };
+    
+    class Flow
+    {
+    public:
+        FlowData* flow_data = nullptr;
+        unsigned flow_data_id = 0;
+        
+        FlowData* get_flow_data(unsigned id) const {
+            if (flow_data and id == flow_data_id)
+                return flow_data;
+            return nullptr;
+        }
+        
+        int set_flow_data(FlowData* fd) {
+            flow_data = fd;
+            if (fd)
+                flow_data_id = SocksFlowData::get_inspector_id();
+            return 0;
+        }
+    };
+    
+    class Trace {};
+    
+    class TraceApi
+    {
+    public:
+        static unsigned get_constraints_generation();
+        static bool filter(const Packet&);
+    };
+    
+    uint32_t FlowData::flow_data_id = 1;
+    FlowData::FlowData(uint32_t, Inspector*) {}
+    FlowData::~FlowData() {}
+    FlowData* FlowDataStore::get(uint32_t) const { return nullptr; }
+    
+    void trace_vprintf(const char*, uint8_t, const char*, const Packet*, const char*, va_list) {}
+    
+    const StreamBuffer StreamSplitter::reassemble(Flow*, unsigned, unsigned,
+        const uint8_t*, unsigned, uint32_t, unsigned&)
+    {
+        return {nullptr, 0};
+    }
+    
+    unsigned StreamSplitter::max(Flow*) { return 0; }
+    
+    unsigned TraceApi::get_constraints_generation() { return 0; }
+    bool TraceApi::filter(const Packet&) { return false; }
+    
+    char* snort_strdup(const char* s)
+    {
+        if (!s)
+            return nullptr;
+        size_t len = strlen(s) + 1;
+        char* dup = new char[len];
+        memcpy(dup, s, len);
+        return dup;
+    }
+}
+
+// Mock SOCKS stats
+THREAD_LOCAL SocksStats socks_stats;
+
+//=============================================================================
+// Test Group: SOCKS Splitter Error Cases
+//=============================================================================
+TEST_GROUP(SocksSplitterErrorCases)
+{
+    SocksSplitter* splitter = nullptr;
+    
+    void setup()
+    {
+        SocksFlowData::init();
+        splitter = new SocksSplitter(true); // client-to-server
+    }
+    
+    void teardown()
+    {
+        delete splitter;
+    }
+};
+
+// Note: scan() tests with Packet are covered by socks_splitter_test.cc
+// This file focuses on testing the private parsing methods directly
+
+// Test auth negotiation with invalid version
+TEST(SocksSplitterErrorCases, test_auth_neg_invalid_version)
+{
+    uint8_t data[] = {0x04, 0x01, 0x00}; // Wrong version for auth negotiation
+    uint32_t result = splitter->parse_auth_negotiation(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test auth negotiation with zero methods
+TEST(SocksSplitterErrorCases, test_auth_neg_zero_methods)
+{
+    uint8_t data[] = {0x05, 0x00}; // Zero methods
+    uint32_t result = splitter->parse_auth_negotiation(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test auth negotiation incomplete
+TEST(SocksSplitterErrorCases, test_auth_neg_incomplete)
+{
+    uint8_t data[] = {0x05, 0x03, 0x00}; // Says 3 methods but only 1 byte
+    uint32_t result = splitter->parse_auth_negotiation(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test auth response too short
+TEST(SocksSplitterErrorCases, test_auth_resp_too_short)
+{
+    uint8_t data[] = {0x05}; // Only 1 byte
+    uint32_t result = splitter->parse_auth_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test auth response with unsupported method (GSSAPI)
+TEST(SocksSplitterErrorCases, test_auth_resp_unsupported_method)
+{
+    uint8_t data[] = {0x05, 0x01}; // GSSAPI (unsupported)
+    uint32_t result = splitter->parse_auth_response(data, sizeof(data));
+    CHECK_EQUAL(2, result); // Consumes the 2 bytes
+}
+
+// Test auth response invalid version
+TEST(SocksSplitterErrorCases, test_auth_resp_invalid_version)
+{
+    uint8_t data[] = {0x03, 0x00}; // Invalid version
+    uint32_t result = splitter->parse_auth_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test username/password auth invalid version
+TEST(SocksSplitterErrorCases, test_userpass_auth_invalid_version)
+{
+    uint8_t data[] = {0x02, 0x04, 'u', 's', 'e', 'r', 0x04, 'p', 'a', 's', 's'};
+    uint32_t result = splitter->parse_username_password_auth(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test username/password auth too short
+TEST(SocksSplitterErrorCases, test_userpass_auth_too_short)
+{
+    uint8_t data[] = {0x01, 0x04}; // Missing username
+    uint32_t result = splitter->parse_username_password_auth(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test username/password auth incomplete username
+TEST(SocksSplitterErrorCases, test_userpass_auth_incomplete_username)
+{
+    uint8_t data[] = {0x01, 0x10, 'u', 's'}; // Says 16 bytes but only 2
+    uint32_t result = splitter->parse_username_password_auth(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test username/password auth incomplete password
+TEST(SocksSplitterErrorCases, test_userpass_auth_incomplete_password)
+{
+    uint8_t data[] = {0x01, 0x04, 'u', 's', 'e', 'r', 0x10, 'p'}; // Says 16 bytes password but only 1
+    uint32_t result = splitter->parse_username_password_auth(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test username/password auth response invalid version
+TEST(SocksSplitterErrorCases, test_userpass_auth_resp_invalid_version)
+{
+    uint8_t data[] = {0x02, 0x00}; // Wrong version
+    uint32_t result = splitter->parse_username_password_auth_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test username/password auth response too short
+TEST(SocksSplitterErrorCases, test_userpass_auth_resp_too_short)
+{
+    uint8_t data[] = {0x01}; // Only 1 byte
+    uint32_t result = splitter->parse_username_password_auth_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test connect request invalid version
+TEST(SocksSplitterErrorCases, test_connect_req_invalid_version)
+{
+    uint8_t data[] = {0x04, 0x01, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};
+    uint32_t result = splitter->parse_connect_request(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test connect request too short
+TEST(SocksSplitterErrorCases, test_connect_req_too_short)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00}; // Only 3 bytes
+    uint32_t result = splitter->parse_connect_request(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test connect response invalid version
+TEST(SocksSplitterErrorCases, test_connect_resp_invalid_version)
+{
+    uint8_t data[] = {0x04, 0x00, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};
+    uint32_t result = splitter->parse_connect_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test connect response too short
+TEST(SocksSplitterErrorCases, test_connect_resp_too_short)
+{
+    uint8_t data[] = {0x05, 0x00, 0x00}; // Only 3 bytes
+    uint32_t result = splitter->parse_connect_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test address parsing with invalid address type
+TEST(SocksSplitterErrorCases, test_address_invalid_type)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0xFF}; // Invalid address type 0xFF
+    uint32_t result = splitter->parse_address_port_length(data, sizeof(data), 3);
+    CHECK_EQUAL(0, result);
+}
+
+// Test address parsing with domain length = 0
+TEST(SocksSplitterErrorCases, test_address_domain_zero_length)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x03, 0x00, 0x00, 0x50}; // Domain length = 0
+    uint32_t result = splitter->parse_address_port_length(data, sizeof(data), 3);
+    CHECK_EQUAL(0, result);
+}
+
+// Test address parsing with domain length > 253
+TEST(SocksSplitterErrorCases, test_address_domain_too_long)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x03, 254}; // Domain length = 254 (> 253 max)
+    uint32_t result = splitter->parse_address_port_length(data, sizeof(data), 3);
+    CHECK_EQUAL(0, result);
+}
+
+// Test address parsing incomplete domain
+TEST(SocksSplitterErrorCases, test_address_domain_incomplete)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x03, 0x0A, 'e', 'x'}; // Says 10 bytes but only 2
+    uint32_t result = splitter->parse_address_port_length(data, sizeof(data), 3);
+    CHECK_EQUAL(0, result);
+}
+
+// Test address parsing offset out of bounds
+TEST(SocksSplitterErrorCases, test_address_offset_out_of_bounds)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x01};
+    uint32_t result = splitter->parse_address_port_length(data, sizeof(data), 10); // Offset > len
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4 request invalid version
+TEST(SocksSplitterErrorCases, test_socks4_req_invalid_version)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01, 0x00};
+    uint32_t result = splitter->parse_socks4_request(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4 request too short
+TEST(SocksSplitterErrorCases, test_socks4_req_too_short)
+{
+    uint8_t data[] = {0x04, 0x01, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01}; // Only 8 bytes
+    uint32_t result = splitter->parse_socks4_request(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4 request no null terminator
+TEST(SocksSplitterErrorCases, test_socks4_req_no_null_terminator)
+{
+    uint8_t data[] = {0x04, 0x01, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01, 'u', 's', 'e', 'r'}; // No null
+    uint32_t result = splitter->parse_socks4_request(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4 request userid too long
+TEST(SocksSplitterErrorCases, test_socks4_req_userid_too_long)
+{
+    uint8_t data[300];
+    data[0] = 0x04; // Version
+    data[1] = 0x01; // Command
+    data[2] = 0x00; data[3] = 0x50; // Port
+    data[4] = 0x0A; data[5] = 0x00; data[6] = 0x00; data[7] = 0x01; // IP
+    // Fill with 256+ bytes (too long)
+    for (int i = 8; i < 270; i++)
+        data[i] = 'A';
+    
+    uint32_t result = splitter->parse_socks4_request(data, 270);
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4a request incomplete domain
+TEST(SocksSplitterErrorCases, test_socks4a_req_incomplete_domain)
+{
+    uint8_t data[] = {0x04, 0x01, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 'e', 'x', 'a', 'm'}; // No null
+    uint32_t result = splitter->parse_socks4_request(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4a request domain too long
+TEST(SocksSplitterErrorCases, test_socks4a_req_domain_too_long)
+{
+    uint8_t data[300];
+    data[0] = 0x04; // Version
+    data[1] = 0x01; // Command
+    data[2] = 0x00; data[3] = 0x50; // Port
+    data[4] = 0x00; data[5] = 0x00; data[6] = 0x00; data[7] = 0x01; // SOCKS4a indicator
+    data[8] = 0x00; // Userid null terminator
+    // Fill with 254+ bytes domain (too long)
+    for (int i = 9; i < 270; i++)
+        data[i] = 'a';
+    
+    uint32_t result = splitter->parse_socks4_request(data, 270);
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4 response too short
+TEST(SocksSplitterErrorCases, test_socks4_resp_too_short)
+{
+    uint8_t data[] = {0x00, 0x5A, 0x00, 0x50, 0x0A, 0x00, 0x00}; // Only 7 bytes
+    uint32_t result = splitter->parse_socks4_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test SOCKS4 response invalid version
+TEST(SocksSplitterErrorCases, test_socks4_resp_invalid_version)
+{
+    uint8_t data[] = {0x04, 0x5A, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01}; // Version should be 0x00
+    uint32_t result = splitter->parse_socks4_response(data, sizeof(data));
+    CHECK_EQUAL(0, result);
+}
+
+// Test valid cases to ensure they still work
+TEST(SocksSplitterErrorCases, test_valid_auth_negotiation)
+{
+    uint8_t data[] = {0x05, 0x02, 0x00, 0x02}; // Version 5, 2 methods
+    uint32_t result = splitter->parse_auth_negotiation(data, sizeof(data));
+    CHECK_EQUAL(4, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_auth_response_none)
+{
+    uint8_t data[] = {0x05, 0x00}; // No auth
+    uint32_t result = splitter->parse_auth_response(data, sizeof(data));
+    CHECK_EQUAL(2, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_auth_response_userpass)
+{
+    uint8_t data[] = {0x05, 0x02}; // Username/password
+    uint32_t result = splitter->parse_auth_response(data, sizeof(data));
+    CHECK_EQUAL(2, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_userpass_auth)
+{
+    uint8_t data[] = {0x01, 0x04, 'u', 's', 'e', 'r', 0x04, 'p', 'a', 's', 's'};
+    uint32_t result = splitter->parse_username_password_auth(data, sizeof(data));
+    CHECK_EQUAL(11, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_userpass_auth_response)
+{
+    uint8_t data[] = {0x01, 0x00}; // Success
+    uint32_t result = splitter->parse_username_password_auth_response(data, sizeof(data));
+    CHECK_EQUAL(2, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_connect_request_ipv4)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};
+    uint32_t result = splitter->parse_connect_request(data, sizeof(data));
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_connect_response_ipv4)
+{
+    uint8_t data[] = {0x05, 0x00, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};
+    uint32_t result = splitter->parse_connect_response(data, sizeof(data));
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_socks4_request)
+{
+    uint8_t data[] = {0x04, 0x01, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01, 0x00};
+    uint32_t result = splitter->parse_socks4_request(data, sizeof(data));
+    CHECK_EQUAL(9, result);
+}
+
+TEST(SocksSplitterErrorCases, test_valid_socks4_response)
+{
+    uint8_t data[] = {0x00, 0x5A, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01};
+    uint32_t result = splitter->parse_socks4_response(data, sizeof(data));
+    CHECK_EQUAL(8, result);
+}
+
+TEST(SocksSplitterErrorCases, test_bind_reverse_auth_response_state)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00};  // Auth negotiation with 1 method
+    uint32_t result = splitter->parse_client_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+    CHECK_EQUAL(3, result);
+}
+
+TEST(SocksSplitterErrorCases, test_bind_reverse_auth_negotiation_state)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};  // Connect request
+    uint32_t result = splitter->parse_client_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_bind_reverse_connect_request_state)
+{
+    uint8_t data[] = {0x05, 0x00, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};  // Connect response
+    uint32_t result = splitter->parse_client_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_bind_reverse_connect_response_state)
+{
+    uint8_t data[] = {0x05, 0x01, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};  // Connect request
+    uint32_t result = splitter->parse_client_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_CONNECT_RESPONSE);
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_default_state_returns_zero)
+{
+    uint8_t data[] = {0x05, 0x01};
+    uint32_t result = splitter->parse_client_packet(data, sizeof(data), static_cast<SocksState>(999));
+    CHECK_EQUAL(0, result);
+}
+
+TEST(SocksSplitterErrorCases, test_server_connect_request_state)
+{
+    uint8_t data[] = {0x05, 0x00, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};  // Connect response
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), SOCKS_STATE_V5_CONNECT_REQUEST);
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_server_bind_reverse_auth_negotiation_state)
+{
+    uint8_t data[] = {0x05, 0x00};  // Auth response
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_AUTH_NEGOTIATION);
+    CHECK_EQUAL(2, result);
+}
+
+TEST(SocksSplitterErrorCases, test_server_bind_reverse_auth_response_state)
+{
+    uint8_t data[] = {0x05, 0x00, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};  // Connect response
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_AUTH_RESPONSE);
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_server_bind_reverse_connect_request_state)
+{
+    uint8_t data[] = {0x05, 0x00, 0x00, 0x01, 0x0A, 0x00, 0x00, 0x01, 0x00, 0x50};  // Connect response
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_CONNECT_REQUEST);
+    CHECK_EQUAL(10, result);
+}
+
+TEST(SocksSplitterErrorCases, test_server_bind_reverse_connect_response_state)
+{
+    uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), SOCKS_STATE_V5_BIND_REVERSE_CONNECT_RESPONSE);
+    CHECK_EQUAL(5, result);  // Returns len
+}
+
+TEST(SocksSplitterErrorCases, test_server_established_state)
+{
+    uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), SOCKS_STATE_ESTABLISHED);
+    CHECK_EQUAL(5, result);  // Returns len
+}
+
+TEST(SocksSplitterErrorCases, test_server_error_state)
+{
+    uint8_t data[] = {0x01, 0x02, 0x03};
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), SOCKS_STATE_ERROR);
+    CHECK_EQUAL(3, result);  // Returns len
+}
+
+TEST(SocksSplitterErrorCases, test_server_default_state_returns_zero)
+{
+    uint8_t data[] = {0x05, 0x01};
+    uint32_t result = splitter->parse_server_packet(data, sizeof(data), static_cast<SocksState>(999));
+    CHECK_EQUAL(0, result);
+}
+
+// Main test runner
+int main(int argc, char** argv)
+{
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
diff --git a/src/service_inspectors/socks/test/socks_splitter_test.cc b/src/service_inspectors/socks/test/socks_splitter_test.cc
new file mode 100644 (file)
index 0000000..5afc898
--- /dev/null
@@ -0,0 +1,615 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025-2026 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+//socks_splitter_test.cc -- author Raza Shafiq <rshafiq@cisco.com>
+
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+#include "../socks_splitter.h"
+#include "../socks_flow_data.h"
+#include "stream/stream_splitter.h"
+
+namespace snort
+{
+    class Flow;
+    class Packet;
+}
+
+class FlushBucket;
+#define PKT_FROM_CLIENT  0x00000080
+#define PKT_FROM_SERVER  0x00000040
+
+using namespace snort;
+
+namespace snort
+{
+    class Packet
+    {
+    public:
+        Flow* flow = nullptr;
+
+        Packet(bool) { flow = nullptr; }
+        ~Packet() = default;
+    };
+
+    class Flow
+    {
+    public:
+        FlowData* flow_data = nullptr;
+        unsigned flow_data_id = 0;
+
+        FlowData* get_flow_data(unsigned id) const {
+            if (flow_data and id == flow_data_id)
+                return flow_data;
+            return nullptr;
+        }
+
+        int set_flow_data(FlowData* fd) {
+            flow_data = fd;
+            if (fd)
+                flow_data_id = SocksFlowData::get_inspector_id();
+            return 0;
+        }
+    };
+
+    class Trace {};
+
+    class TraceApi
+    {
+    public:
+        static unsigned get_constraints_generation();
+        static bool filter(const Packet&);
+    };
+
+    uint32_t FlowData::flow_data_id = 1;
+    FlowData::FlowData(uint32_t, Inspector*) {}
+    FlowData::~FlowData() {}
+    FlowData* FlowDataStore::get(uint32_t) const { return nullptr; }
+
+    void trace_vprintf(const char*, uint8_t, const char*, const Packet*, const char*, va_list) {}
+
+    const StreamBuffer StreamSplitter::reassemble(Flow*, unsigned, unsigned,
+        const uint8_t*, unsigned, uint32_t, unsigned&)
+    {
+        return {nullptr, 0};
+    }
+
+    unsigned StreamSplitter::max(Flow*) { return 0; }
+
+    unsigned TraceApi::get_constraints_generation() { return 0; }
+    bool TraceApi::filter(const Packet&) { return false; }
+}
+
+// Mock for socks_stats - must match real definition in socks_module.h
+using PegCount = uint64_t;
+struct SocksStats
+{
+    PegCount sessions = 0;
+    PegCount concurrent_sessions = 0;
+    PegCount max_concurrent_sessions = 0;
+    PegCount auth_requests = 0;
+    PegCount auth_successes = 0;
+    PegCount auth_failures = 0;
+    PegCount connect_requests = 0;
+    PegCount bind_requests = 0;
+    PegCount udp_associate_requests = 0;
+    PegCount successful_connections = 0;
+    PegCount failed_connections = 0;
+    PegCount udp_associations_created = 0;
+    PegCount udp_expectations_created = 0;
+    PegCount udp_packets = 0;
+    PegCount udp_frags_dropped = 0;
+    PegCount udp_frags_blocked = 0;
+};
+THREAD_LOCAL SocksStats socks_stats;
+
+class FlushBucket
+{
+public:
+    static uint32_t get_size() { return 16384; }
+};
+
+// ===== TEST GROUP =====
+
+// Global initialization - call init() once before any tests
+struct SocksTestInit
+{
+    SocksTestInit() { SocksFlowData::init(); }
+};
+static SocksTestInit socks_test_init;
+
+TEST_GROUP(SocksSplitterTests)
+{
+    SocksSplitter* client_splitter;
+    SocksSplitter* server_splitter;
+    Packet* packet;
+    Flow* flow;
+    SocksFlowData* flow_data;
+
+    void setup()
+    {
+        SocksFlowData::init();
+
+        client_splitter = new SocksSplitter(true);
+        server_splitter = new SocksSplitter(false);
+        packet = new Packet(false);
+        flow = new Flow();
+        flow_data = new SocksFlowData();
+
+        packet->flow = flow;
+        flow->set_flow_data(flow_data);
+    }
+
+    void teardown()
+    {
+        delete client_splitter;
+        delete server_splitter;
+        delete packet;
+        delete flow_data;
+        delete flow;
+    }
+};
+
+TEST(SocksSplitterTests, test_flow_data_retrieval)
+{
+    CHECK(packet != nullptr);
+    CHECK(packet->flow != nullptr);
+    CHECK(packet->flow == flow);
+
+    unsigned id = SocksFlowData::get_inspector_id();
+    CHECK(flow->get_flow_data(id) != nullptr);
+    CHECK(flow->get_flow_data(id) == flow_data);
+
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_RESPONSE);
+    CHECK_EQUAL(SOCKS_STATE_V4_CONNECT_RESPONSE, flow_data->get_state());
+
+    uint8_t data[] = {0x00, 0x5A, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01};
+    uint32_t fp = 0;
+
+    StreamSplitter::Status result = server_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_SERVER, &fp);
+
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK_EQUAL(8, fp);
+}
+
+TEST(SocksSplitterTests, test_is_paf)
+{
+    CHECK_TRUE(client_splitter->is_paf());
+    CHECK_TRUE(server_splitter->is_paf());
+}
+
+
+TEST(SocksSplitterTests, test_scan_null_data)
+{
+    uint32_t fp = 0;
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, nullptr, 10, PKT_FROM_CLIENT, &fp);
+    CHECK_EQUAL(StreamSplitter::SEARCH, result);
+}
+
+TEST(SocksSplitterTests, test_socks4_partial_request)
+{
+    uint8_t data[] = {
+        0x04, 0x01,             // VER=4, CMD=CONNECT
+        0x00, 0x50,             // PORT=80
+        0x0A, 0x00, 0x00, 0x01  // IP=10.0.0.1 (no userid)
+    };
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_CLIENT, &fp);
+
+    CHECK_EQUAL(StreamSplitter::SEARCH, result);
+}
+
+TEST(SocksSplitterTests, test_socks4_response)
+{
+    uint8_t data[] = {
+        0x00, 0x5A,             // VER=0, REP=0x5A (granted)
+        0x00, 0x50,             // PORT=80
+        0x0A, 0x00, 0x00, 0x01  // IP=10.0.0.1
+    };
+    uint32_t fp = 0;
+
+    // SOCKS4 response is 8 bytes: VER(1) + REP(1) + PORT(2) + IP(4)
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_RESPONSE);
+    StreamSplitter::Status result = server_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_SERVER, &fp);
+
+    // SOCKS4 Protocol: Complete 8-byte response MUST return FLUSH
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK_EQUAL(8, fp);
+}
+
+// ===== SOCKS4a AUTO-DETECTION TESTS =====
+
+TEST(SocksSplitterTests, test_socks4a_with_domain_auto_detect)
+{
+    // SOCKS4a with domain name (IP starts with 0.0.0.x)
+    uint8_t data[] = {
+        0x04, 0x01,             // VER=4, CMD=CONNECT
+        0x00, 0x50,             // PORT=80
+        0x00, 0x00, 0x00, 0x01, // IP=0.0.0.1 (signals domain follows)
+        0x75, 0x73, 0x65, 0x72, 0x00,  // userid="user\0"
+        'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x00  // domain="example.com\0"
+    };
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_CLIENT, &fp);
+
+    // SOCKS4a Protocol: Complete message MUST return FLUSH with exact length
+    // VER(1) + CMD(1) + PORT(2) + IP(4) + USERID(5) + DOMAIN(12) = 25 bytes
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK_EQUAL(25, fp);
+}
+
+// ===== SOCKS5 AUTO-DETECTION TESTS =====
+
+// Note: SOCKS5 CONNECT request tests removed - state-specific parsing
+// not fully implemented. Tests were using weak assertions that don't
+// verify correct behavior. Need to either:
+// 1. Implement proper state-specific parsing in splitter
+// 2. Test only auto-detection path (INIT state)
+// For now, focusing on tests that verify actual working functionality.
+
+// ===== VERSION DISCRIMINATION TESTS =====
+
+TEST(SocksSplitterTests, test_auto_detect_socks4_vs_socks5)
+{
+    // Test that splitter can distinguish SOCKS4 (0x04) from SOCKS5 (0x05)
+    uint8_t socks4_data[] = {0x04, 0x01, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01, 0x00};
+    uint8_t socks5_data[] = {0x05, 0x01, 0x00};
+    uint32_t fp4 = 0, fp5 = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result4 = client_splitter->scan(
+        packet, socks4_data, sizeof(socks4_data), PKT_FROM_CLIENT, &fp4);
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result5 = client_splitter->scan(
+        packet, socks5_data, sizeof(socks5_data), PKT_FROM_CLIENT, &fp5);
+
+    // SOCKS4 complete message should FLUSH
+    CHECK_EQUAL(StreamSplitter::FLUSH, result4);
+    CHECK(fp4 > 0);
+
+    // SOCKS5 complete auth negotiation should FLUSH
+    CHECK_EQUAL(StreamSplitter::FLUSH, result5);
+    CHECK_EQUAL(3, fp5);
+}
+
+TEST(SocksSplitterTests, test_auto_detect_socks4_vs_socks4a)
+{
+    // SOCKS4 with real IP
+    uint8_t socks4_data[] = {
+        0x04, 0x01, 0x00, 0x50,
+        0x0A, 0x00, 0x00, 0x01,  // Real IP: 10.0.0.1
+        0x00
+    };
+
+    // SOCKS4a with 0.0.0.x IP (signals domain)
+    uint8_t socks4a_data[] = {
+        0x04, 0x01, 0x00, 0x50,
+        0x00, 0x00, 0x00, 0x01,  // Special IP: 0.0.0.1
+        0x00,
+        'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x00
+    };
+
+    uint32_t fp4 = 0, fp4a = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result4 = client_splitter->scan(
+        packet, socks4_data, sizeof(socks4_data), PKT_FROM_CLIENT, &fp4);
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result4a = client_splitter->scan(
+        packet, socks4a_data, sizeof(socks4a_data), PKT_FROM_CLIENT, &fp4a);
+
+    // Both complete messages should FLUSH
+    CHECK_EQUAL(StreamSplitter::FLUSH, result4);
+    CHECK_EQUAL(9, fp4);
+
+    CHECK_EQUAL(StreamSplitter::FLUSH, result4a);
+    CHECK(fp4a > 9);  // Should be longer due to domain
+}
+
+// ===== COMPLEX SCENARIO TESTS =====
+
+TEST(SocksSplitterTests, test_fragmented_socks5_auth_negotiation)
+{
+    // Simulate fragmented packets - first fragment
+    uint8_t frag1[] = {0x05, 0x03};  // VER + NMETHODS (incomplete)
+    uint32_t fp1 = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result1 = client_splitter->scan(
+        packet, frag1, sizeof(frag1), PKT_FROM_CLIENT, &fp1);
+
+    // Should need more data
+    CHECK_EQUAL(StreamSplitter::SEARCH, result1);
+
+    // Second fragment with methods
+    uint8_t frag2[] = {0x05, 0x03, 0x00, 0x01, 0x02};  // Complete message
+    uint32_t fp2 = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result2 = client_splitter->scan(
+        packet, frag2, sizeof(frag2), PKT_FROM_CLIENT, &fp2);
+
+    // Should flush complete message
+    CHECK_EQUAL(StreamSplitter::FLUSH, result2);
+}
+
+TEST(SocksSplitterTests, test_socks5_with_all_auth_methods)
+{
+    // SOCKS5 with all common auth methods
+    uint8_t data[] = {
+        0x05, 0x04,                 // VER=5, NMETHODS=4
+        0x00,                       // NO_AUTH
+        0x01,                       // GSSAPI
+        0x02,                       // USERNAME/PASSWORD
+        0x03                        // CHAP
+    };
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_CLIENT, &fp);
+
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK_EQUAL(6, fp);
+}
+
+TEST(SocksSplitterTests, test_socks4_with_long_userid)
+{
+    // SOCKS4 with long userid
+    uint8_t data[200];
+    int idx = 0;
+    data[idx++] = 0x04; data[idx++] = 0x01;  // VER=4, CMD=CONNECT
+    data[idx++] = 0x00; data[idx++] = 0x50;  // PORT=80
+    data[idx++] = 0x0A; data[idx++] = 0x00;  // IP=10.0.0.1
+    data[idx++] = 0x00; data[idx++] = 0x01;
+
+    // Long userid
+    for (int i = 0; i < 100; i++) {
+        data[idx++] = 'a' + (i % 26);
+    }
+    data[idx++] = 0x00;  // Null terminator
+
+    uint32_t fp = 0;
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, idx, PKT_FROM_CLIENT, &fp);
+
+    // SOCKS4 Protocol: Complete request with long userid (100 chars)
+    // VER(1) + CMD(1) + PORT(2) + IP(4) + USERID(101) = 109 bytes
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK_EQUAL(109, fp);
+}
+
+// ===== EDGE CASE TESTS =====
+
+TEST(SocksSplitterTests, test_invalid_version_byte)
+{
+    // Invalid SOCKS version (not 0x04 or 0x05)
+    uint8_t data[] = {0x99, 0x01, 0x00};
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_CLIENT, &fp);
+
+    // Splitter doesn't recognize version 0x99, returns SEARCH (needs more data)
+    // This is correct behavior - might not be SOCKS at all
+    CHECK_EQUAL(StreamSplitter::SEARCH, result);
+}
+
+TEST(SocksSplitterTests, test_minimum_packet_size)
+{
+    // Too small to be any valid SOCKS message
+    uint8_t data[] = {0x05};
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, 1, PKT_FROM_CLIENT, &fp);
+
+    // Too small - returns SEARCH (needs more data)
+    CHECK_EQUAL(StreamSplitter::SEARCH, result);
+}
+
+TEST(SocksSplitterTests, test_large_buffer_handling)
+{
+    // Large buffer with valid SOCKS5 auth at start
+    uint8_t data[1000];
+    data[0] = 0x05;
+    data[1] = 0x01;
+    data[2] = 0x00;
+    // Fill rest with garbage
+    for (int i = 3; i < 1000; i++) data[i] = 0xAA;
+
+    uint32_t fp = 0;
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, 1000, PKT_FROM_CLIENT, &fp);
+
+    // Should flush just the SOCKS message, not the whole buffer
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK(fp >= 3 and fp < 1000);
+}
+
+// ===== SERVER RESPONSE TESTS =====
+// (Covered by direction handling tests)
+
+// ===== DIRECTION HANDLING TESTS =====
+
+TEST(SocksSplitterTests, test_client_flag_required)
+{
+    // Without PKT_FROM_CLIENT flag, should be treated as server packet
+    uint8_t data[] = {0x05, 0x01, 0x00};
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_INIT);
+    // Pass 0 flags - will be treated as server
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, sizeof(data), 0, &fp);
+
+    // Without client flag, treated as server - may auto-detect
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+}
+
+TEST(SocksSplitterTests, test_server_flag_required)
+{
+    // Server response needs PKT_FROM_SERVER flag
+    uint8_t data[] = {0x05, 0x00};
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_V5_AUTH_NEGOTIATION);
+    StreamSplitter::Status result = server_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_SERVER, &fp);
+
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+}
+
+// ===== STATE MACHINE TESTS =====
+
+
+TEST(SocksSplitterTests, test_error_state_handling)
+{
+    // In error state, should handle gracefully
+    uint8_t data[] = {0x05, 0x01, 0x00};
+    uint32_t fp = 0;
+
+    flow_data->set_state(SOCKS_STATE_ERROR);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, sizeof(data), PKT_FROM_CLIENT, &fp);
+
+    // SOCKS Protocol: In ERROR state, should still pass data through
+    // MUST return FLUSH to allow error handling at higher layers
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK_EQUAL(3, fp);
+}
+
+// ===== ROBUSTNESS TESTS =====
+
+TEST(SocksSplitterTests, test_max_methods_boundary)
+{
+    // Test with maximum number of methods (255)
+    uint8_t data[257];
+    data[0] = 0x05;    // version
+    data[1] = 0xFF;    // 255 methods
+    for (int i = 2; i < 257; i++) data[i] = 0x00;
+
+    uint32_t fp = 0;
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result = client_splitter->scan(
+        packet, data, 257, PKT_FROM_CLIENT, &fp);
+
+    CHECK_EQUAL(StreamSplitter::FLUSH, result);
+    CHECK_EQUAL(257, fp);
+}
+
+// ===== FIELD-LEVEL PARSING VALIDATION TESTS =====
+// These tests verify individual field parsing (overlaps removed)
+
+TEST(SocksSplitterTests, test_socks4_userid_null_terminator_required)
+{
+    // Test that userid MUST be null-terminated
+    uint8_t with_null[] = {0x04, 0x01, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01, 'u', 's', 'e', 'r', 0x00};
+    uint8_t without_null[] = {0x04, 0x01, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01, 'u', 's', 'e', 'r'};
+    uint32_t fp = 0;
+
+    // With null terminator - should parse completely
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result_with = client_splitter->scan(
+        packet, with_null, sizeof(with_null), PKT_FROM_CLIENT, &fp);
+    CHECK_EQUAL(StreamSplitter::FLUSH, result_with);
+    CHECK_EQUAL(13, fp);
+
+    // Without null terminator - should need more data
+    fp = 0;
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result_without = client_splitter->scan(
+        packet, without_null, sizeof(without_null), PKT_FROM_CLIENT, &fp);
+    CHECK_EQUAL(StreamSplitter::SEARCH, result_without);  // Needs null terminator
+}
+
+TEST(SocksSplitterTests, test_socks4a_domain_null_terminator_required)
+{
+    // SOCKS4a domain must also be null-terminated
+    uint8_t with_null[] = {0x04, 0x01, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 
+                           'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x00};
+    uint8_t without_null[] = {0x04, 0x01, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00,
+                              'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'};
+    uint32_t fp = 0;
+
+    // With null terminator
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result_with = client_splitter->scan(
+        packet, with_null, sizeof(with_null), PKT_FROM_CLIENT, &fp);
+    CHECK_EQUAL(StreamSplitter::FLUSH, result_with);
+    CHECK_EQUAL(21, fp);
+
+    // Without null terminator - should need more data
+    fp = 0;
+    flow_data->set_state(SOCKS_STATE_INIT);
+    StreamSplitter::Status result_without = client_splitter->scan(
+        packet, without_null, sizeof(without_null), PKT_FROM_CLIENT, &fp);
+    CHECK_EQUAL(StreamSplitter::SEARCH, result_without);
+}
+
+TEST(SocksSplitterTests, test_socks4_response_version_field)
+{
+    // SOCKS4 response version must be 0x00 (not 0x04!)
+    uint8_t valid[] = {0x00, 0x5A, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01};
+    uint8_t invalid[] = {0x04, 0x5A, 0x00, 0x50, 0x0A, 0x00, 0x00, 0x01};  // Wrong version
+    uint32_t fp = 0;
+
+    // Valid response (VER=0x00)
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_RESPONSE);
+    StreamSplitter::Status result_valid = server_splitter->scan(
+        packet, valid, sizeof(valid), PKT_FROM_SERVER, &fp);
+    CHECK_EQUAL(StreamSplitter::FLUSH, result_valid);
+    CHECK_EQUAL(8, fp);
+
+    // Invalid response (VER=0x04)
+    fp = 0;
+    flow_data->set_state(SOCKS_STATE_V4_CONNECT_RESPONSE);
+    StreamSplitter::Status result_invalid = server_splitter->scan(
+        packet, invalid, sizeof(invalid), PKT_FROM_SERVER, &fp);
+    CHECK_EQUAL(StreamSplitter::SEARCH, result_invalid);  // Should reject
+}
+
+
+// ===== RUN ALL TESTS =====
+
+int main(int ac, char** av)
+{
+    MemoryLeakWarningPlugin::turnOffNewDeleteOverloads();
+    return CommandLineTestRunner::RunAllTests(ac, av);
+}
index 4fc444cb04fceac5ffffd9ce743a29dc8e7fed35..96f24dec724f786867ce5fcf965ab3dadd236f24 100644 (file)
@@ -13,6 +13,8 @@ set(FILE_LIST
     opcua_curse.h
     s7commplus_curse.cc
     s7commplus_curse.h
+    socks_curse.cc
+    socks_curse.h
     ssl_curse.cc
     ssl_curse.h
     hexes.cc
@@ -34,4 +36,5 @@ add_catch_test(curses_test
     NO_TEST_SOURCE
     SOURCES
         ssl_curse.cc
+        socks_curse.cc
 )
index 71d33c06d837e7c6aa6707a98c429cea4e63325c..f273e228fd5448b6957d0ff5afd2e50cc73d6c66 100644 (file)
@@ -38,6 +38,7 @@ vector<CurseDetails> CurseBook::curse_map =
     { "opcua"     , "opcua"      , CurseBook::opcua_curse     , true  },
     { "s7commplus", "s7commplus" , CurseBook::s7commplus_curse, true  },
     { "dce_smb"   , "netbios-ssn", CurseBook::dce_smb_curse   , true  },
+    { "socks"     , "socks"      , CurseBook::socks_curse     , true  },
     { "sslv2"     , "ssl"        , CurseBook::ssl_v2_curse    , true  }
 };
 
index 78014f8695a0094260524df68184678778a3d56b..3e5a3e9c1b2cbb71bd6e8c7f3f80f110b10eece7 100644 (file)
@@ -29,6 +29,7 @@
 #include "mms_curse.h"
 #include "opcua_curse.h"
 #include "s7commplus_curse.h"
+#include "socks_curse.h"
 #include "ssl_curse.h"
 
 class CurseTracker
@@ -38,6 +39,7 @@ public:
     MmsTracker mms;
     OpcuaTracker opcua;
     S7commplusTracker s7commplus;
+    SocksTracker socks;
     SslTracker ssl;
 };
 
@@ -71,6 +73,7 @@ private:
 #ifdef CATCH_TEST_BUILD
 public:
 #endif
+    static bool socks_curse(const uint8_t* data, unsigned len, CurseTracker*);
     static bool ssl_v2_curse(const uint8_t* data, unsigned len, CurseTracker*);
 };
 
diff --git a/src/service_inspectors/wizard/socks_curse.cc b/src/service_inspectors/wizard/socks_curse.cc
new file mode 100644 (file)
index 0000000..5003f44
--- /dev/null
@@ -0,0 +1,1207 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_curse.cc - author Raza Shafiq <rshafiq@cisco.com>
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "socks_curse.h"
+#include "curse_book.h"
+
+// SOCKS protocol constants
+#define SOCKS4_VERSION 0x04
+#define SOCKS5_VERSION 0x05
+
+#define SOCKS_CMD_CONNECT       0x01
+#define SOCKS_CMD_BIND          0x02
+#define SOCKS_CMD_UDP_ASSOCIATE 0x03
+
+// SOCKS5 authentication methods
+#define SOCKS5_AUTH_NONE              0x00
+#define SOCKS5_AUTH_GSSAPI            0x01
+#define SOCKS5_AUTH_USERNAME_PASSWORD 0x02
+#define SOCKS5_AUTH_NO_ACCEPTABLE     0xFF
+
+// Limits to prevent DoS
+#define SOCKS_MAX_USERID_LEN  255
+#define SOCKS_MAX_DOMAIN_LEN  255
+#define SOCKS5_MAX_METHODS    32
+
+bool CurseBook::socks_curse(const uint8_t* data, unsigned len, CurseTracker* tracker)
+{
+    SocksTracker& socks = tracker->socks;
+
+    // Check terminal states first (matches ssl_curse.cc pattern)
+    // NOT_FOUND is sticky: assumes initial invocation begins at protocol boundary
+    // (start of application data in correct direction). This avoids repeated work
+    // on flows that are clearly not SOCKS.
+    if ( socks.state == SOCKS_STATE__NOT_FOUND )
+        return false;
+    else if ( socks.state == SOCKS_STATE__FOUND )
+        return true;
+
+    for ( unsigned idx = 0; idx < len; ++idx )
+    {
+        uint8_t val = data[idx];
+
+        switch ( socks.state )
+        {
+            case SOCKS_STATE__VERSION:
+            {
+                // SOCKS version: 0x04 (SOCKS4) or 0x05 (SOCKS5)
+                if ( val == SOCKS4_VERSION )
+                {
+                    socks.version = val;
+                    socks.state = SOCKS_STATE__V4_COMMAND;
+                }
+                else if ( val == SOCKS5_VERSION )
+                {
+                    socks.version = val;
+                    socks.state = SOCKS_STATE__V5_NMETHODS;
+                }
+                else
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                break;
+            }
+
+            // ==================== SOCKS4/4a States ====================
+
+            case SOCKS_STATE__V4_COMMAND:
+            {
+                // SOCKS4: VER(1) CMD(1) PORT(2) IP(4) USERID(variable) NULL(1)
+                // SOCKS4a: VER(1) CMD(1) PORT(2) IP=0.0.0.x(4) USERID(var) NULL(1) DOMAIN(var) NULL(1)
+                // Validate command is CONNECT or BIND
+                if ( val == SOCKS_CMD_CONNECT || val == SOCKS_CMD_BIND )
+                {
+                    socks.command = val;
+                    socks.state = SOCKS_STATE__V4_PORT_MSB;
+                }
+                else
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                break;
+            }
+
+            case SOCKS_STATE__V4_PORT_MSB:
+            {
+                // Port MSB - store it (network byte order, MSB first)
+                socks.port = (uint16_t)val << 8;
+                socks.state = SOCKS_STATE__V4_PORT_LSB;
+                break;
+            }
+
+            case SOCKS_STATE__V4_PORT_LSB:
+            {
+                // Port LSB - complete port
+                socks.port |= val;
+                
+                // Validate port: Port 0 is invalid for CONNECT
+                // (BIND can have port 0 meaning server chooses)
+                if ( socks.command == SOCKS_CMD_CONNECT && socks.port == 0 )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                
+                socks.state = SOCKS_STATE__V4_IP_1;
+                socks.ip_addr = 0;
+                break;
+            }
+
+            case SOCKS_STATE__V4_IP_1:
+            {
+                // First IP octet (most significant)
+                socks.ip_addr = (uint32_t)val << 24;
+                socks.state = SOCKS_STATE__V4_IP_2;
+                break;
+            }
+
+            case SOCKS_STATE__V4_IP_2:
+            {
+                // Second IP octet
+                socks.ip_addr |= (uint32_t)val << 16;
+                socks.state = SOCKS_STATE__V4_IP_3;
+                break;
+            }
+
+            case SOCKS_STATE__V4_IP_3:
+            {
+                // Third IP octet
+                socks.ip_addr |= (uint32_t)val << 8;
+                socks.state = SOCKS_STATE__V4_IP_4;
+                break;
+            }
+
+            case SOCKS_STATE__V4_IP_4:
+            {
+                // Last IP octet - complete the address
+                socks.ip_addr |= val;
+                
+                // Determine if this is SOCKS4a (domain name follows)
+                // SOCKS4a uses IP 0.0.0.x where x != 0 to indicate domain name follows
+                uint8_t first_octet = (socks.ip_addr >> 24) & 0xFF;
+                
+                // Initialize is_socks4a to false
+                socks.is_socks4a = false;
+                
+                if ( socks.command == SOCKS_CMD_BIND )
+                {
+                    // BIND command: IP 0.0.0.0 is valid (server chooses address)
+                    // Any IP is acceptable for BIND
+                    // No validation needed - proceed to userid
+                }
+                else if ( socks.command == SOCKS_CMD_CONNECT )
+                {
+                    // CONNECT command: validate IP address
+                    if ( first_octet == 0 )
+                    {
+                        if ( socks.ip_addr == 0 )
+                        {
+                            // 0.0.0.0 is invalid for CONNECT
+                            socks.state = SOCKS_STATE__NOT_FOUND;
+                            return false;
+                        }
+                        // Check if it's SOCKS4a format: 0.0.0.x where x > 0
+                        if ( (socks.ip_addr & 0xFFFFFF00) != 0 )
+                        {
+                            // Not 0.0.0.x format (e.g., 0.1.2.3) - invalid
+                            socks.state = SOCKS_STATE__NOT_FOUND;
+                            return false;
+                        }
+                        // Valid SOCKS4a indicator: 0.0.0.1-255
+                        socks.is_socks4a = true;
+                    }
+                    // Reject loopback and multicast for CONNECT
+                    else if ( first_octet == 127 || first_octet >= 224 )
+                    {
+                        socks.state = SOCKS_STATE__NOT_FOUND;
+                        return false;
+                    }
+                }
+                
+                // Now expect USERID (variable length, null-terminated)
+                socks.state = SOCKS_STATE__V4_USERID;
+                socks.userid_length = 0;
+                break;
+            }
+
+            case SOCKS_STATE__V4_USERID:
+            {
+                // USERID is variable length, terminated by NULL byte
+                // Empty USERID (just NULL terminator) is valid per RFC
+                if ( val == 0x00 )
+                {
+                    // Found null terminator for userid
+                    if ( socks.is_socks4a )
+                    {
+                        // SOCKS4a: Domain name follows the userid null
+                        socks.state = SOCKS_STATE__V4A_DOMAIN;
+                        socks.domain_length = 0;
+                    }
+                    else
+                    {
+                        // Regular SOCKS4: We're done
+                        socks.state = SOCKS_STATE__FOUND;
+                        return true;
+                    }
+                }
+                else
+                {
+                    // SOCKS4 USERID: any byte except NULL is valid per RFC
+                    // Real-world clients may use non-ASCII bytes
+                    if ( socks.userid_length >= SOCKS_MAX_USERID_LEN )
+                    {
+                        // Userid too long - DoS protection
+                        socks.state = SOCKS_STATE__NOT_FOUND;
+                        return false;
+                    }
+                    socks.userid_length++;
+                }
+                break;
+            }
+
+            case SOCKS_STATE__V4A_DOMAIN:
+            {
+                // SOCKS4a domain name: variable length, null-terminated
+                // Detector heuristic: allow common hostname/domain bytes
+                // (alphanumeric, hyphen, dot, underscore for robustness)
+                // Strict validation reduces false positives vs other protocols
+                if ( val == 0x00 )
+                {
+                    // Found null terminator for domain
+                    // Domain must be at least 1 character
+                    if ( socks.domain_length == 0 )
+                    {
+                        socks.state = SOCKS_STATE__NOT_FOUND;
+                        return false;
+                    }
+                    
+                    socks.state = SOCKS_STATE__FOUND;
+                    return true;
+                }
+                // Allow hostname-like characters (underscore included for robustness)
+                else if ( (val >= 'a' && val <= 'z') ||
+                          (val >= 'A' && val <= 'Z') ||
+                          (val >= '0' && val <= '9') ||
+                          val == '-' || val == '.' || val == '_' )
+                {
+                    if ( socks.domain_length >= SOCKS_MAX_DOMAIN_LEN )
+                    {
+                        // Domain too long
+                        socks.state = SOCKS_STATE__NOT_FOUND;
+                        return false;
+                    }
+                    socks.domain_length++;
+                }
+                else
+                {
+                    // Domain heuristic reject: non-hostname character
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                break;
+            }
+
+            // ==================== SOCKS5 States ====================
+
+            case SOCKS_STATE__V5_NMETHODS:
+            {
+                // SOCKS5 Client Greeting: VER(1) NMETHODS(1) METHODS(1-255)
+                // RFC 1928 Section 3
+                socks.nmethods = val;
+
+                // RFC 1928: NMETHODS must be at least 1
+                if ( socks.nmethods == 0 )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+
+                // Practical limit: Real clients don't send many methods
+                // Standard methods (0x00-0x09) + private (0x80-0xFE) = ~32 is generous
+                if ( socks.nmethods > SOCKS5_MAX_METHODS )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+
+                // Reset method tracking fields (important for segmented data)
+                socks.unique_methods = 0;
+                socks.has_common = false;
+                socks.saw_duplicate = false;
+                socks.v5_confirm_budget = 0;
+                socks.methods_seen[0] = 0;
+                socks.methods_seen[1] = 0;
+                socks.methods_seen[2] = 0;
+                socks.methods_seen[3] = 0;
+
+                // Note: Don't check total length here - handle segmentation
+                // by processing methods one at a time in V5_METHODS state
+                socks.methods_remaining = socks.nmethods;
+                socks.state = SOCKS_STATE__V5_METHODS;
+                break;
+            }
+
+            case SOCKS_STATE__V5_METHODS:
+            {
+                // Validate method values per RFC 1928:
+                // 0x00-0x09: IANA assigned methods
+                // 0x03-0x7F: IANA assigned/reserved (allow for robustness)
+                // 0x80-0xFE: Private methods
+                // 0xFF: NO ACCEPTABLE METHODS (server-only - reject)
+                if ( val == 0xFF )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                
+                // LPD false positive prevention: reject 0x0A (LF) as a method value
+                // RFC 1179: LPD commands are terminated with LF (0x0A)
+                // While 0x0A is technically in the IANA range, it's never used in practice
+                // and its presence is a strong indicator of LPD traffic, not SOCKS5
+                if ( val == 0x0A )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                
+                // Track method using 256-bit bitmask
+                unsigned mask_word = val / 64;
+                uint64_t bit = 1ULL << (val % 64);
+                
+                if ( socks.methods_seen[mask_word] & bit )
+                {
+                    // Already seen this method - duplicate
+                    socks.saw_duplicate = true;
+                }
+                else
+                {
+                    // New unique method
+                    socks.methods_seen[mask_word] |= bit;
+                    socks.unique_methods++;
+                    
+                    // Check for common methods (0x00, 0x01, 0x02)
+                    if ( val <= 0x02 )
+                        socks.has_common = true;
+                }
+                
+                if ( socks.methods_remaining > 0 )
+                    socks.methods_remaining--;
+                
+                if ( socks.methods_remaining == 0 )
+                {
+                    // Greeting complete - apply validation to avoid false positives
+                    
+                    // LPD false positive check: Extra bytes after greeting
+                    // LPD pattern: greeting followed by 0x0A (line feed terminator)
+                    // RFC 1179: LPD commands are terminated with LF (0x0A)
+                    // This byte is never valid in SOCKS5 (not a version byte, not in any valid position)
+                    unsigned remaining = len - (idx + 1);
+                    if ( remaining > 0 )
+                    {
+                        const uint8_t* p = &data[idx + 1];
+                        
+                        // Specifically reject 0x0A (LF) as it's the LPD terminator
+                        // Don't reject other values - they might be legitimate SOCKS5 or need more analysis
+                        if ( p[0] == 0x0A )
+                        {
+                            socks.state = SOCKS_STATE__NOT_FOUND;
+                            return false;
+                        }
+                    }
+                    
+                    // Fast accept for greetings with common methods (no RTT delay)
+                    // Common methods (0x00-0x02) are strong SOCKS5 indicators
+                    if ( socks.has_common )
+                    {
+                        socks.state = SOCKS_STATE__FOUND;
+                        return true;
+                    }
+                    
+                    // No common methods - apply stricter checks to avoid false positives
+                    // Trade-off: Private/non-common method auth exchanges that occur BEFORE
+                    // the request header will cause NOT_FOUND. This prioritizes false-positive
+                    // suppression over detection of rare private-auth SOCKS5 implementations.
+                    
+                    // Check: nmethods >= 2 but only 1 unique NON-COMMON method
+                    // Example: 0x05 0x05 0x05 0x05 0x05 (all same non-common method)
+                    if ( socks.nmethods >= 2 && socks.unique_methods == 1 )
+                    {
+                        socks.state = SOCKS_STATE__NOT_FOUND;
+                        return false;
+                    }
+                    
+                    // Slow path: Only non-common methods - need 4-byte request header to confirm
+                    // This adds ~1 RTT delay but only for rare non-common-method greetings
+                    socks.state = SOCKS_STATE__V5_REQ_VER;
+                    socks.v5_confirm_budget = 8;
+                }
+                break;
+            }
+
+            // 4-byte request header confirmation states
+            // Validates: VER(0x05) CMD(0x01-0x03) RSV(0x00) ATYP(0x01/0x03/0x04)
+            // Budget prevents staying in these states forever on trickle/junk data
+            case SOCKS_STATE__V5_REQ_VER:
+            {
+                if ( socks.v5_confirm_budget == 0 )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                socks.v5_confirm_budget--;
+
+                if ( val == SOCKS5_VERSION )
+                {
+                    socks.state = SOCKS_STATE__V5_REQ_CMD;
+                }
+                else
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                break;
+            }
+
+            case SOCKS_STATE__V5_REQ_CMD:
+            {
+                if ( socks.v5_confirm_budget == 0 )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                socks.v5_confirm_budget--;
+
+                // CMD must be CONNECT(1), BIND(2), or UDP_ASSOCIATE(3)
+                if ( val >= SOCKS_CMD_CONNECT && val <= SOCKS_CMD_UDP_ASSOCIATE )
+                {
+                    socks.state = SOCKS_STATE__V5_REQ_RSV;
+                }
+                else
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                break;
+            }
+
+            case SOCKS_STATE__V5_REQ_RSV:
+            {
+                if ( socks.v5_confirm_budget == 0 )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                socks.v5_confirm_budget--;
+
+                // RSV must be 0x00
+                if ( val == 0x00 )
+                {
+                    socks.state = SOCKS_STATE__V5_REQ_ATYP;
+                }
+                else
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                break;
+            }
+
+            case SOCKS_STATE__V5_REQ_ATYP:
+            {
+                if ( socks.v5_confirm_budget == 0 )
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+                socks.v5_confirm_budget--;
+
+                // ATYP must be IPv4(1), Domain(3), or IPv6(4)
+                if ( val == 0x01 || val == 0x03 || val == 0x04 )
+                {
+                    socks.state = SOCKS_STATE__FOUND;
+                    return true;
+                }
+                else
+                {
+                    socks.state = SOCKS_STATE__NOT_FOUND;
+                    return false;
+                }
+            }
+
+            case SOCKS_STATE__FOUND:
+            case SOCKS_STATE__NOT_FOUND:
+                // These are handled at function entry - should not reach here
+                // but keep for safety
+                return (socks.state == SOCKS_STATE__FOUND);
+
+            default:
+                // Unknown state - should never happen
+                socks.state = SOCKS_STATE__NOT_FOUND;
+                return false;
+        }
+    }
+
+    // Ran out of data before completing detection
+    // Return false but DON'T change state - allows continuation with more data
+    // This is critical for handling TCP segmentation
+    return false;
+}
+
+#ifdef CATCH_TEST_BUILD
+
+#include "catch/catch.hpp"
+#include <cstring>
+
+// ==================== Valid SOCKS5 Test Data ====================
+// Detection logic (POLICY):
+// - Fast accept if common methods (0x00-0x02) and no suspicious suffix โ†’ FOUND (no RTT delay)
+// - Reject if nmethods >= 2 but only 1 unique non-common method โ†’ NOT_FOUND (pattern attack)
+// - Reject if extra bytes after greeting don't parse as valid C2S request header โ†’ NOT_FOUND (LPD/junk)
+// - Slow path for non-common methods only โ†’ need 4-byte request header to confirm
+//   VER(0x05) CMD(0x01-0x03) RSV(0x00) ATYP(0x01/0x03/0x04)
+
+// Valid SOCKS5: 1 method (NO_AUTH = 0x00) + 4-byte request header to confirm
+// Greeting: VER(0x05) NMETHODS(0x01) METHOD(0x00)
+// Request: VER(0x05) CMD(0x01=CONNECT) RSV(0x00) ATYP(0x01=IPv4)
+static const uint8_t socks5_1method[] = { 0x05, 0x01, 0x00, 0x05, 0x01, 0x00, 0x01 };
+
+// Valid SOCKS5: 2 methods (NO_AUTH, USERNAME_PASSWORD) + 4-byte request header
+static const uint8_t socks5_2methods[] = { 0x05, 0x02, 0x00, 0x02, 0x05, 0x01, 0x00, 0x01 };
+
+// Valid SOCKS5: 3 methods (NO_AUTH, GSSAPI, USERNAME_PASSWORD) + 4-byte request header
+static const uint8_t socks5_3methods[] = { 0x05, 0x03, 0x00, 0x01, 0x02, 0x05, 0x01, 0x00, 0x01 };
+
+// Valid SOCKS5: private method with NO_AUTH + 4-byte request header
+static const uint8_t socks5_private_method[] = { 0x05, 0x02, 0x00, 0x80, 0x05, 0x01, 0x00, 0x01 };
+
+// ==================== Valid SOCKS4 Test Data ====================
+
+// Valid SOCKS4 CONNECT: port 80, IP 192.168.1.1, empty userid
+static const uint8_t socks4_connect_empty_userid[] = { 
+    0x04, 0x01,              // VER, CMD (CONNECT)
+    0x00, 0x50,              // PORT (80)
+    0xC0, 0xA8, 0x01, 0x01,  // IP (192.168.1.1)
+    0x00                     // USERID (empty, just null)
+};
+
+// Valid SOCKS4 CONNECT: port 443, IP 8.8.8.8, userid "user"
+static const uint8_t socks4_connect_with_userid[] = { 
+    0x04, 0x01,              // VER, CMD (CONNECT)
+    0x01, 0xBB,              // PORT (443)
+    0x08, 0x08, 0x08, 0x08,  // IP (8.8.8.8)
+    'u', 's', 'e', 'r', 0x00 // USERID "user"
+};
+
+// Valid SOCKS4 BIND: port 0 (server chooses), IP 10.0.0.1
+static const uint8_t socks4_bind[] = { 
+    0x04, 0x02,              // VER, CMD (BIND)
+    0x00, 0x00,              // PORT (0 - server chooses)
+    0x0A, 0x00, 0x00, 0x01,  // IP (10.0.0.1)
+    0x00                     // USERID (empty)
+};
+
+// Valid SOCKS4 BIND: port 0, IP 0.0.0.0 (server chooses both) - like the test pcap!
+static const uint8_t socks4_bind_all_zeros[] = { 
+    0x04, 0x02,              // VER, CMD (BIND)
+    0x00, 0x00,              // PORT (0 - server chooses)
+    0x00, 0x00, 0x00, 0x00,  // IP (0.0.0.0 - server chooses)
+    'u', 's', 'e', 'r', '1', 0x00  // USERID "user1"
+};
+
+// ==================== Valid SOCKS4a Test Data ====================
+
+// Valid SOCKS4a CONNECT: domain "example.com"
+static const uint8_t socks4a_connect[] = { 
+    0x04, 0x01,              // VER, CMD (CONNECT)
+    0x00, 0x50,              // PORT (80)
+    0x00, 0x00, 0x00, 0x01,  // IP (0.0.0.1 = SOCKS4a indicator)
+    0x00,                    // USERID (empty)
+    'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x00  // DOMAIN
+};
+
+// Valid SOCKS4a with userid and domain
+static const uint8_t socks4a_with_userid[] = { 
+    0x04, 0x01,              // VER, CMD
+    0x01, 0xBB,              // PORT (443)
+    0x00, 0x00, 0x00, 0xFF,  // IP (0.0.0.255 = SOCKS4a indicator)
+    'u', 's', 'e', 'r', 0x00,// USERID "user"
+    't', 'e', 's', 't', '.', 'c', 'o', 'm', 0x00  // DOMAIN "test.com"
+};
+
+// ==================== Invalid Test Data ====================
+
+// Invalid: wrong version
+static const uint8_t invalid_version[] = { 0x03, 0x01, 0x00 };
+
+// Invalid: SOCKS5 with 0 methods
+static const uint8_t socks5_zero_methods[] = { 0x05, 0x00 };
+
+// Invalid: SOCKS5 with NO_ACCEPTABLE (0xFF is server-only)
+static const uint8_t socks5_ff_method[] = { 0x05, 0x01, 0xFF };
+
+// Invalid: SOCKS4 with invalid command
+static const uint8_t socks4_invalid_cmd[] = { 0x04, 0x03, 0x00, 0x50, 0xC0, 0xA8, 0x01, 0x01, 0x00 };
+
+// Invalid: SOCKS4 CONNECT with port 0
+static const uint8_t socks4_connect_port0[] = { 0x04, 0x01, 0x00, 0x00, 0xC0, 0xA8, 0x01, 0x01, 0x00 };
+
+// Invalid: SOCKS4 with loopback IP
+static const uint8_t socks4_loopback[] = { 0x04, 0x01, 0x00, 0x50, 0x7F, 0x00, 0x00, 0x01, 0x00 };
+
+// Invalid: SOCKS4 with multicast IP
+static const uint8_t socks4_multicast[] = { 0x04, 0x01, 0x00, 0x50, 0xE0, 0x00, 0x00, 0x01, 0x00 };
+
+// Invalid: SOCKS4 with 0.0.0.0 IP
+static const uint8_t socks4_zero_ip[] = { 0x04, 0x01, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 };
+
+// Invalid: SOCKS4 with 0.x.x.x IP (not 0.0.0.1-255)
+static const uint8_t socks4_invalid_zero_ip[] = { 0x04, 0x01, 0x00, 0x50, 0x00, 0x01, 0x02, 0x03, 0x00 };
+
+// Invalid: SOCKS4a with empty domain
+static const uint8_t socks4a_empty_domain[] = { 
+    0x04, 0x01, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00  // no domain chars
+};
+
+// ==================== Edge Cases (Allowed for Robustness) ====================
+
+// Edge case: SOCKS5 with reserved method (0x0A-0x7F) + common method + 4-byte request header
+static const uint8_t socks5_reserved_method[] = { 0x05, 0x02, 0x00, 0x50, 0x05, 0x01, 0x00, 0x01 };
+
+// Edge case: SOCKS5 with only reserved method - needs 4-byte request header to confirm
+// Greeting: VER(0x05) NMETHODS(0x01) METHOD(0x50=reserved)
+// Request: VER(0x05) CMD(0x01=CONNECT) RSV(0x00) ATYP(0x01=IPv4)
+static const uint8_t socks5_only_reserved[] = { 0x05, 0x01, 0x50, 0x05, 0x01, 0x00, 0x01 };
+
+// Edge case: SOCKS4 with non-printable byte in USERID - allowed per RFC
+static const uint8_t socks4_nonprintable_userid[] = { 
+    0x04, 0x01, 0x00, 0x50, 0xC0, 0xA8, 0x01, 0x01, 0x01, 0x00  // userid has 0x01
+};
+
+// ==================== Tests ====================
+
+// Helper to build SOCKS5 packet with 32 methods (max allowed) + 4-byte request header
+// Array reference enforces buffer size at compile time
+// 34 bytes for greeting (VER + NMETHODS + 32 methods) + 4 bytes for request header = 38 bytes
+// Includes method 0x00 (NO_AUTH) so has_common=true
+static void build_socks5_32methods(uint8_t (&buffer)[38])
+{
+    buffer[0] = 0x05;  // version
+    buffer[1] = 32;    // nmethods
+    // Use different methods: 0-9 (skip 0x0A which is rejected as LPD), then 11-32
+    // This avoids 0x0A (LF) which is LPD line feed terminator, never used in SOCKS5
+    for (int i = 0; i < 32; i++)
+    {
+        uint8_t method = (uint8_t)i;
+        if (method >= 10)  // Skip over 0x0A by shifting up by 1
+            method++;
+        buffer[2 + i] = method;  // methods: 0-9, 11-33 (includes 0x00, 0x01, 0x02)
+    }
+    // 4-byte request header to confirm
+    buffer[34] = 0x05;  // VER
+    buffer[35] = 0x01;  // CMD (CONNECT)
+    buffer[36] = 0x00;  // RSV
+    buffer[37] = 0x01;  // ATYP (IPv4)
+}
+
+TEST_CASE("socks5 greeting detection", "[SocksCurse]")
+{
+    SECTION("common method - completes immediately")
+    {
+        // RFC 1928: Common methods 0x00 (NO_AUTH), 0x01 (GSSAPI), 0x02 (USERNAME_PASSWORD)
+        // Wizard binds immediately, inspector validates
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks5_2methods, sizeof(socks5_2methods), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.nmethods == 2);
+        CHECK(tracker.socks.has_common == true);
+    }
+
+    SECTION("non-common method - requires request header")
+    {
+        // Non-common methods need 4-byte request header confirmation
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks5_only_reserved, sizeof(socks5_only_reserved), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.has_common == false);
+    }
+
+    SECTION("max methods (32) - boundary")
+    {
+        // RFC 1928: NMETHODS field is 1 byte, but reasonable limit is 32
+        uint8_t socks5_32methods[38];
+        build_socks5_32methods(socks5_32methods);
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks5_32methods, sizeof(socks5_32methods), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.nmethods == 32);
+    }
+
+    SECTION("excessive methods (33) - sanity reject")
+    {
+        // Exceeds reasonable limit - sanity check
+        uint8_t greeting[35];
+        greeting[0] = 0x05;
+        greeting[1] = 0x21; // 33 methods
+        for (int i = 0; i < 33; i++)
+            greeting[2 + i] = (i < 3) ? i : (i + 0x10);
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(greeting, sizeof(greeting), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+        // NOT_FOUND is sticky within a tracker - verify it stays NOT_FOUND
+        CHECK(false == CurseBook::socks_curse(socks5_1method, sizeof(socks5_1method), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+        // Fresh tracker with valid data should work
+        CurseTracker tracker2{};
+        CHECK(true == CurseBook::socks_curse(socks5_1method, sizeof(socks5_1method), &tracker2));
+        CHECK(tracker2.socks.state == SOCKS_STATE__FOUND);
+    }
+}
+
+TEST_CASE("segmentation handling", "[SocksCurse]")
+{
+    SECTION("SOCKS5 byte-by-byte")
+    {
+        // RFC 1928: VER(1) + NMETHODS(1) + METHODS(N)
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(&socks5_2methods[0], 1, &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__V5_NMETHODS);
+        
+        CHECK(false == CurseBook::socks_curse(&socks5_2methods[1], 1, &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__V5_METHODS);
+        
+        CHECK(false == CurseBook::socks_curse(&socks5_2methods[2], 1, &tracker));
+        CHECK(tracker.socks.has_common == true);
+        
+        CHECK(true == CurseBook::socks_curse(&socks5_2methods[3], 1, &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+    }
+
+    SECTION("SOCKS4 byte-by-byte")
+    {
+        // RFC 1928: VER(1) + CMD(1) + PORT(2) + IP(4) + USERID(N) + NULL(1)
+        CurseTracker tracker{};
+        const uint8_t* data = socks4_connect_empty_userid;
+        unsigned len = sizeof(socks4_connect_empty_userid);
+        
+        for (unsigned i = 0; i < len - 1; i++)
+            CHECK(false == CurseBook::socks_curse(&data[i], 1, &tracker));
+        
+        CHECK(true == CurseBook::socks_curse(&data[len-1], 1, &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+    }
+
+    SECTION("SOCKS4a domain split")
+    {
+        // RFC 1928: Domain can be split across packets
+        const uint8_t socks4a_full[] = { 
+            0x04, 0x01, 0x00, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00,
+            'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x00
+        };
+        
+        CurseTracker tracker{};
+        const size_t split_point = 14; // Split in middle of "example.com"
+        CHECK(false == CurseBook::socks_curse(socks4a_full, split_point, &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__V4A_DOMAIN);
+        
+        const size_t remaining = sizeof(socks4a_full) - split_point;
+        CHECK(true == CurseBook::socks_curse(&socks4a_full[split_point], remaining, &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.domain_length == 11);
+    }
+}
+
+TEST_CASE("socks5 edge cases", "[SocksCurse]")
+{
+    SECTION("reserved method with common method (allowed)")
+    {
+        // Reserved methods (0x0A-0x7F) are ALLOWED when combined with common method
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks5_reserved_method, sizeof(socks5_reserved_method), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.has_common == true);
+    }
+
+    SECTION("only reserved method - needs 4-byte request header to confirm")
+    {
+        // Reserved method alone needs full 4-byte request header to confirm
+        // VER(0x05) CMD(0x01) RSV(0x00) ATYP(0x01)
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks5_only_reserved, sizeof(socks5_only_reserved), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+    }
+
+    SECTION("only reserved method - no confirmation data")
+    {
+        // Reserved method alone without request header - stays in V5_REQ_VER
+        CurseTracker tracker{};
+        uint8_t only_reserved_no_confirm[] = { 0x05, 0x01, 0x50 };  // no follow-up
+        CHECK(false == CurseBook::socks_curse(only_reserved_no_confirm, sizeof(only_reserved_no_confirm), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__V5_REQ_VER);
+    }
+
+    SECTION("only reserved method - wrong request header VER")
+    {
+        // Reserved method with wrong VER in request header -> NOT_FOUND
+        CurseTracker tracker{};
+        uint8_t wrong_ver[] = { 0x05, 0x01, 0x50, 0x04 };  // VER=0x04 is wrong
+        CHECK(false == CurseBook::socks_curse(wrong_ver, sizeof(wrong_ver), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("only reserved method - wrong request header CMD")
+    {
+        // Reserved method with wrong CMD in request header -> NOT_FOUND
+        CurseTracker tracker{};
+        uint8_t wrong_cmd[] = { 0x05, 0x01, 0x50, 0x05, 0x04 };  // CMD=0x04 is invalid
+        CHECK(false == CurseBook::socks_curse(wrong_cmd, sizeof(wrong_cmd), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("only reserved method - wrong request header RSV")
+    {
+        // Reserved method with wrong RSV in request header -> NOT_FOUND
+        CurseTracker tracker{};
+        uint8_t wrong_rsv[] = { 0x05, 0x01, 0x50, 0x05, 0x01, 0x01 };  // RSV=0x01 is wrong
+        CHECK(false == CurseBook::socks_curse(wrong_rsv, sizeof(wrong_rsv), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("only reserved method - wrong request header ATYP")
+    {
+        // Reserved method with wrong ATYP in request header -> NOT_FOUND
+        CurseTracker tracker{};
+        uint8_t wrong_atyp[] = { 0x05, 0x01, 0x50, 0x05, 0x01, 0x00, 0x02 };  // ATYP=0x02 is invalid
+        CHECK(false == CurseBook::socks_curse(wrong_atyp, sizeof(wrong_atyp), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+}
+
+TEST_CASE("socks5 invalid input", "[SocksCurse]")
+{
+    SECTION("zero methods")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks5_zero_methods, sizeof(socks5_zero_methods), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("0xFF method (server-only)")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks5_ff_method, sizeof(socks5_ff_method), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("wrong version")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(invalid_version, sizeof(invalid_version), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("too many methods (> 32)")
+    {
+        CurseTracker tracker{};
+        uint8_t too_many[] = { 0x05, 0x40 };  // 64 methods > 32 limit
+        CHECK(false == CurseBook::socks_curse(too_many, sizeof(too_many), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("LPD false positive - 0x0A as extra byte after greeting")
+    {
+        // Real-world LPD: 05 05 05 05 41 41 00 0a (8 bytes from actual traffic)
+        // NMETHODS=5, methods: 05,05,41,41,00, then extra byte 0x0A (LF terminator)
+        // This mimics SOCKS5 but is actually printer protocol
+        CurseTracker tracker{};
+        uint8_t lpd_pattern[] = { 0x05, 0x05, 0x05, 0x05, 0x41, 0x41, 0x00, 0x0A };
+        
+        CHECK(false == CurseBook::socks_curse(lpd_pattern, sizeof(lpd_pattern), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("LPD false positive - 0x0A as method value")
+    {
+        // Real-world LPD: 05 05 05 05 00 0a (6 bytes)
+        // NMETHODS=5, methods: 05,05,05,00,0x0A
+        // 0x0A as a method value is LPD line feed terminator
+        CurseTracker tracker{};
+        uint8_t lpd_with_lf_method[] = { 0x05, 0x05, 0x05, 0x05, 0x00, 0x0A };
+        
+        CHECK(false == CurseBook::socks_curse(lpd_with_lf_method, sizeof(lpd_with_lf_method), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("SOCKS5 greeting with 0x00 after - should accept")
+    {
+        // Real SOCKS5 from socks_sessions PCAP: 05 06 00 01 00 00 00 00 [00]
+        // NMETHODS=6, methods: 00,01,00,00,00,00 (has common method 0x00)
+        // Extra 0x00 byte should NOT be rejected (only 0x0A is LPD-specific)
+        CurseTracker tracker{};
+        uint8_t with_null[] = { 0x05, 0x06, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00 };
+        
+        CHECK(true == CurseBook::socks_curse(with_null, sizeof(with_null), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.has_common == true);
+    }
+
+    SECTION("repeated common method - needs request header to confirm")
+    {
+        // Repeated common method (0x00) needs request header to confirm
+        // This is "stupid but valid" - a client sending duplicate NO_AUTH
+        CurseTracker tracker{};
+        uint8_t all_same[] = { 0x05, 0x02, 0x00, 0x00, 0x05, 0x01, 0x00, 0x01 };  // greeting + request
+        CHECK(true == CurseBook::socks_curse(all_same, sizeof(all_same), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.has_common == true);
+        CHECK(tracker.socks.unique_methods == 1);
+        CHECK(tracker.socks.saw_duplicate == true);
+    }
+
+    SECTION("duplicates OK if multiple unique methods")
+    {
+        // Duplicates are fine as long as there are multiple unique methods
+        CurseTracker tracker{};
+        uint8_t with_dups[] = { 0x05, 0x04, 0x00, 0x00, 0x01, 0x01, 0x05, 0x01, 0x00, 0x01 };  // greeting + request
+        CHECK(true == CurseBook::socks_curse(with_dups, sizeof(with_dups), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.unique_methods == 2);
+        CHECK(tracker.socks.saw_duplicate == true);
+        CHECK(tracker.socks.has_common == true);
+    }
+
+    SECTION("nmethods=5 with common method - needs request header")
+    {
+        // nmethods=5 is fine if it has common methods, but needs request header
+        CurseTracker tracker{};
+        uint8_t valid_5methods[] = { 0x05, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x01, 0x00, 0x01 };
+        CHECK(true == CurseBook::socks_curse(valid_5methods, sizeof(valid_5methods), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.has_common == true);
+    }
+
+    SECTION("nmethods=1 with single method - needs request header")
+    {
+        // Single method needs request header to confirm
+        CurseTracker tracker{};
+        uint8_t single[] = { 0x05, 0x01, 0x00, 0x05, 0x01, 0x00, 0x01 };  // greeting + request
+        CHECK(true == CurseBook::socks_curse(single, sizeof(single), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+    }
+
+    SECTION("pattern attack - all identical methods (the real attack)")
+    {
+        // This is the actual attack pattern: 0x05 0x05 0x05 0x05 0x05 0x05
+        // VER=5, NMETHODS=5, all 5 methods are 0x05
+        // Rejected because nmethods >= 2 but unique_methods == 1
+        CurseTracker tracker{};
+        uint8_t all_fives[] = { 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05 };
+        CHECK(false == CurseBook::socks_curse(all_fives, sizeof(all_fives), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+        CHECK(tracker.socks.unique_methods == 1);
+    }
+
+    SECTION("nmethods=5 first_method=0x05 but mixed methods - needs request header")
+    {
+        // nmethods=5 with first method=0x05 is ALLOWED if it has common methods
+        // This was previously rejected by the overfitting rule
+        CurseTracker tracker{};
+        uint8_t mixed[] = { 0x05, 0x05, 0x05, 0x00, 0x01, 0x02, 0x03, 0x05, 0x01, 0x00, 0x01 };  // greeting + request
+        CHECK(true == CurseBook::socks_curse(mixed, sizeof(mixed), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.has_common == true);
+        CHECK(tracker.socks.unique_methods == 5);
+    }
+}
+
+TEST_CASE("socks4 and socks4a detection", "[SocksCurse]")
+{
+    SECTION("SOCKS4 CONNECT")
+    {
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks4_connect_with_userid, 
+            sizeof(socks4_connect_with_userid), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.version == 0x04);
+        CHECK(tracker.socks.command == SOCKS_CMD_CONNECT);
+        CHECK(tracker.socks.port == 443);
+        CHECK(tracker.socks.userid_length == 4);
+        CHECK(tracker.socks.is_socks4a == false);
+    }
+
+    SECTION("SOCKS4 BIND - port 0 allowed")
+    {
+        // RFC 1928: port 0 means server chooses
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks4_bind, sizeof(socks4_bind), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.command == SOCKS_CMD_BIND);
+        CHECK(tracker.socks.port == 0);
+    }
+
+    SECTION("SOCKS4a with domain")
+    {
+        // RFC 1928: SOCKS4a uses IP 0.0.0.x to signal domain name follows
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks4a_with_userid, sizeof(socks4a_with_userid), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.is_socks4a == true);
+        CHECK(tracker.socks.userid_length == 4);
+        CHECK(tracker.socks.domain_length == 8);
+    }
+}
+
+TEST_CASE("socks4 invalid input", "[SocksCurse]")
+{
+    SECTION("invalid command")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks4_invalid_cmd, sizeof(socks4_invalid_cmd), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("CONNECT with port 0")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks4_connect_port0, sizeof(socks4_connect_port0), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("loopback IP")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks4_loopback, sizeof(socks4_loopback), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("multicast IP")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks4_multicast, sizeof(socks4_multicast), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("0.0.0.0 IP")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks4_zero_ip, sizeof(socks4_zero_ip), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("invalid 0.x.x.x IP")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks4_invalid_zero_ip, sizeof(socks4_invalid_zero_ip), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("non-printable userid (allowed)")
+    {
+        // Non-printable bytes in USERID are ALLOWED per SOCKS4 RFC
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks4_nonprintable_userid, sizeof(socks4_nonprintable_userid), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+    }
+
+    SECTION("socks4a empty domain")
+    {
+        CurseTracker tracker{};
+        CHECK(false == CurseBook::socks_curse(socks4a_empty_domain, sizeof(socks4a_empty_domain), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+}
+
+TEST_CASE("not_found state is sticky", "[SocksCurse]")
+{
+    CurseTracker tracker{};
+    // First, fail detection
+    CHECK(false == CurseBook::socks_curse(invalid_version, sizeof(invalid_version), &tracker));
+    CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    
+    // Even with valid data, should stay NOT_FOUND
+    CHECK(false == CurseBook::socks_curse(socks5_1method, sizeof(socks5_1method), &tracker));
+    CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+}
+
+TEST_CASE("socks4 length boundaries", "[SocksCurse]")
+{
+    SECTION("userid 255 bytes - max allowed")
+    {
+        // VER + CMD + PORT + IP + 255 userid bytes + NULL
+        uint8_t socks4_max_userid[1 + 1 + 2 + 4 + 255 + 1];
+        socks4_max_userid[0] = 0x04;  // version
+        socks4_max_userid[1] = 0x01;  // CONNECT
+        socks4_max_userid[2] = 0x00;  // port MSB
+        socks4_max_userid[3] = 0x50;  // port LSB (80)
+        socks4_max_userid[4] = 0xC0;  // IP 192.168.1.1
+        socks4_max_userid[5] = 0xA8;
+        socks4_max_userid[6] = 0x01;
+        socks4_max_userid[7] = 0x01;
+        for (int i = 0; i < 255; i++)
+            socks4_max_userid[8 + i] = 'A';  // 255 'A's
+        socks4_max_userid[sizeof(socks4_max_userid) - 1] = 0x00;  // NULL terminator
+        
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks4_max_userid, sizeof(socks4_max_userid), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.userid_length == 255);
+    }
+
+    SECTION("domain 255 bytes - max allowed")
+    {
+        // SOCKS4a: VER + CMD + PORT + SOCKS4a_IP + USERID(empty) + 255 domain bytes + NULL
+        uint8_t socks4a_max_domain[1 + 1 + 2 + 4 + 1 + 255 + 1];
+        socks4a_max_domain[0] = 0x04;  // version
+        socks4a_max_domain[1] = 0x01;  // CONNECT
+        socks4a_max_domain[2] = 0x00;  // port MSB
+        socks4a_max_domain[3] = 0x50;  // port LSB
+        socks4a_max_domain[4] = 0x00;  // SOCKS4a indicator
+        socks4a_max_domain[5] = 0x00;
+        socks4a_max_domain[6] = 0x00;
+        socks4a_max_domain[7] = 0x01;
+        socks4a_max_domain[8] = 0x00;  // empty userid
+        for (int i = 0; i < 255; i++)
+            socks4a_max_domain[9 + i] = 'a';  // 255 'a's
+        socks4a_max_domain[sizeof(socks4a_max_domain) - 1] = 0x00;  // NULL terminator
+        
+        CurseTracker tracker{};
+        CHECK(true == CurseBook::socks_curse(socks4a_max_domain, sizeof(socks4a_max_domain), &tracker));
+        CHECK(tracker.socks.state == SOCKS_STATE__FOUND);
+        CHECK(tracker.socks.domain_length == 255);
+    }
+
+    SECTION("userid 256 bytes - reject")
+    {
+        INFO("Testing that 256-byte USERID is rejected even with NULL terminator present");
+        // VER + CMD + PORT + IP + 256 userid bytes + NULL
+        // Proves rejection is due to length, not missing terminator
+        uint8_t socks4_over_userid[1 + 1 + 2 + 4 + 256 + 1];
+        socks4_over_userid[0] = 0x04;  // version
+        socks4_over_userid[1] = 0x01;  // CONNECT
+        socks4_over_userid[2] = 0x00;  // port MSB
+        socks4_over_userid[3] = 0x50;  // port LSB
+        socks4_over_userid[4] = 0xC0;  // IP 192.168.1.1
+        socks4_over_userid[5] = 0xA8;
+        socks4_over_userid[6] = 0x01;
+        socks4_over_userid[7] = 0x01;
+        for (int i = 0; i < 256; i++)
+            socks4_over_userid[8 + i] = 'A';  // 256 'A's
+        socks4_over_userid[sizeof(socks4_over_userid) - 1] = 0x00;  // NULL terminator
+        
+        CurseTracker tracker{};
+        // Should fail when 256th byte is processed (userid_length >= 255)
+        // Even with terminator present, over-limit is rejected
+        CHECK(false == CurseBook::socks_curse(socks4_over_userid, sizeof(socks4_over_userid), &tracker));
+        INFO("Expected NOT_FOUND, got state=" << static_cast<int>(tracker.socks.state));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+
+    SECTION("domain 256 bytes - reject")
+    {
+        // SOCKS4a: RFC 1928 limit is 255 bytes
+        uint8_t socks4a_over_domain[1 + 1 + 2 + 4 + 1 + 256 + 1];
+        socks4a_over_domain[0] = 0x04;  // version
+        socks4a_over_domain[1] = 0x01;  // CONNECT
+        socks4a_over_domain[2] = 0x00;  // port MSB
+        socks4a_over_domain[3] = 0x50;  // port LSB
+        socks4a_over_domain[4] = 0x00;  // SOCKS4a indicator
+        socks4a_over_domain[5] = 0x00;
+        socks4a_over_domain[6] = 0x00;
+        socks4a_over_domain[7] = 0x01;
+        socks4a_over_domain[8] = 0x00;  // empty userid
+        for (int i = 0; i < 256; i++)
+            socks4a_over_domain[9 + i] = 'a';  // 256 'a's
+        socks4a_over_domain[sizeof(socks4a_over_domain) - 1] = 0x00;  // NULL terminator
+        
+        CurseTracker tracker{};
+        // Should fail when 256th domain byte is processed (domain_length >= 255)
+        // Even with terminator present, over-limit is rejected
+        CHECK(false == CurseBook::socks_curse(socks4a_over_domain, sizeof(socks4a_over_domain), &tracker));
+        INFO("Expected NOT_FOUND, got state=" << static_cast<int>(tracker.socks.state));
+        CHECK(tracker.socks.state == SOCKS_STATE__NOT_FOUND);
+    }
+}
+
+#endif
diff --git a/src/service_inspectors/wizard/socks_curse.h b/src/service_inspectors/wizard/socks_curse.h
new file mode 100644 (file)
index 0000000..65cf230
--- /dev/null
@@ -0,0 +1,69 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// socks_curse.h - author Raza Shafiq <rshafiq@cisco.com>
+
+#ifndef SOCKS_CURSE_H
+#define SOCKS_CURSE_H
+
+#include <cstdint>
+
+enum SocksCurseState
+{
+    SOCKS_STATE__VERSION = 0,
+    SOCKS_STATE__V4_COMMAND,
+    SOCKS_STATE__V4_PORT_MSB,
+    SOCKS_STATE__V4_PORT_LSB,
+    SOCKS_STATE__V4_IP_1,
+    SOCKS_STATE__V4_IP_2,
+    SOCKS_STATE__V4_IP_3,
+    SOCKS_STATE__V4_IP_4,
+    SOCKS_STATE__V4_USERID,
+    SOCKS_STATE__V4A_DOMAIN,
+    SOCKS_STATE__V5_NMETHODS,
+    SOCKS_STATE__V5_METHODS,
+    // 4-byte request header confirmation states (no common methods case)
+    // Confirms SOCKS5 by validating: VER(0x05) CMD(0x01-0x03) RSV(0x00) ATYP(0x01/0x03/0x04)
+    SOCKS_STATE__V5_REQ_VER,
+    SOCKS_STATE__V5_REQ_CMD,
+    SOCKS_STATE__V5_REQ_RSV,
+    SOCKS_STATE__V5_REQ_ATYP,
+    SOCKS_STATE__FOUND,
+    SOCKS_STATE__NOT_FOUND
+};
+
+struct SocksTracker
+{
+    SocksCurseState state = SOCKS_STATE__VERSION;
+    uint8_t version = 0;
+    uint8_t command = 0;
+    uint8_t nmethods = 0;
+    uint8_t methods_remaining = 0;
+    uint16_t port = 0;
+    uint32_t ip_addr = 0;
+    uint8_t userid_length = 0;
+    uint8_t domain_length = 0;
+    bool is_socks4a = false;
+    // Method tracking for false positive detection
+    uint64_t methods_seen[4] = {};  // 256-bit bitmask (4 x 64 bits)
+    uint8_t unique_methods = 0;     // Count of unique methods seen
+    bool saw_duplicate = false;     // True if any method repeated
+    bool has_common = false;        // True if saw 0x00, 0x01, or 0x02
+    uint8_t v5_confirm_budget = 0;  // Limits bytes spent in V5_REQ_* confirm states
+};
+
+#endif
index 8000904392c90023be137c89c0bb13c5eeb1c76c..9b859a28384f9668f49f439a646804b1b856252a 100644 (file)
@@ -96,7 +96,7 @@ static const Parameter s_params[] =
     { "spells", Parameter::PT_LIST, wizard_spells_params, nullptr,
       "criteria for text service identification" },
 
-    { "curses", Parameter::PT_MULTI, "dce_smb | dce_udp | dce_tcp | mms | opcua | s7commplus | sslv2", nullptr,
+    { "curses", Parameter::PT_MULTI, "dce_smb | dce_udp | dce_tcp | mms | opcua | s7commplus | socks | sslv2", nullptr,
       "enable service identification based on internal algorithm" },
 
     { "max_search_depth", Parameter::PT_INT, "0:65535", "8192",
index b840bba1595dbba62427b96bb3080e1fb1b900f1..9f48257659ad823321ac6a8e20b07ecf34d1c4f3 100644 (file)
@@ -494,6 +494,18 @@ void Stream::set_splitter(Flow* flow, bool to_server, StreamSplitter* ss)
     return flow->session->set_splitter(to_server, ss);
 }
 
+uint32_t Stream::get_paf_position(Flow* flow, bool to_server)
+{
+    assert(flow && flow->session);
+    return flow->session->get_paf_position(to_server);
+}
+
+void Stream::set_splitter_with_rescan(Flow* flow, bool to_server, StreamSplitter* ss, uint32_t seq)
+{
+    assert(flow && flow->session);
+    return flow->session->set_splitter_with_rescan(to_server, ss, seq);
+}
+
 StreamSplitter* Stream::get_splitter(Flow* flow, bool to_server)
 {
     assert(flow && flow->session);
index 7d40285eb25bec4f5aa5c59d7074e0d11466350e..6d3eaace76653871a155a0d76b36a854ca833deb 100644 (file)
@@ -189,6 +189,8 @@ public:
     static void init_active_response(const Packet*, Flow*);
 
     static void set_splitter(Flow*, bool toServer, StreamSplitter* = nullptr);
+    static void set_splitter_with_rescan(Flow*, bool toServer, StreamSplitter*, uint32_t seq);
+    static uint32_t get_paf_position(Flow*, bool toServer);
     static StreamSplitter* get_splitter(Flow*, bool toServer);
 
     // Turn off inspection for potential session. Adds session identifiers to a hash table.
index 96439d73437a3c729348814ba89a0128bb4a0d2c..756a64cd998d1ce69ccb82b78f97e49e6c819a60 100644 (file)
@@ -74,6 +74,8 @@ public:
     virtual void initialize_paf() = 0;
     virtual void reset_paf() = 0;
     virtual void clear_paf() = 0;
+    virtual void reinitialize_paf(uint32_t seq) = 0;
+    virtual uint32_t get_paf_position() const = 0;
 
     // static methods for TcpReassembler per thread initialization and termination
     static void tinit();
@@ -133,6 +135,12 @@ public:
     void initialize_paf() override
     { }
 
+    void reinitialize_paf(uint32_t) override
+    { }
+
+    uint32_t get_paf_position() const override
+    { return 0; }
+
     FlushPolicy get_flush_policy() const override
     { return STREAM_FLPOLICY_IGNORE; }
 
@@ -192,6 +200,12 @@ public:
     void clear_paf() override
     { paf.paf_clear(); }
 
+    void reinitialize_paf(uint32_t seq) override
+    { paf.paf_initialize(seq); }
+
+    uint32_t get_paf_position() const override
+    { return paf.seq_num; }
+
 protected:
     void show_rebuilt_packet(snort::Packet*);
     int flush_data_segments(uint32_t flush_len, snort::Packet* pdu);
index c286998534442a7dc2b189faa17dd8504876520d..1577d40923cd6d06c16a935bc965c22819947f7d 100644 (file)
@@ -1361,6 +1361,19 @@ void TcpSession::set_splitter(bool to_server, StreamSplitter* ss)
     trk.set_splitter(ss);
 }
 
+uint32_t TcpSession::get_paf_position(bool to_server) const
+{
+    const TcpStreamTracker& trk = ( to_server ) ? server : client;
+    return trk.get_paf_position();
+}
+
+void TcpSession::set_splitter_with_rescan(bool to_server, StreamSplitter* ss, uint32_t seq)
+{
+    TcpStreamTracker& trk = ( to_server ) ? server : client;
+
+    trk.set_splitter_with_rescan(ss, seq);
+}
+
 uint16_t TcpSession::get_mss(bool to_server) const
 {
     const TcpStreamTracker& trk = (to_server) ? client : server;
index 7eab3707157de4525eadd8319e2d670f357e7720..9e4f0d80217f935bdaf8ef8c488b96f5f3053ec9 100644 (file)
@@ -56,6 +56,8 @@ public:
     void cleanup(snort::Packet* = nullptr) override;
 
     void set_splitter(bool, snort::StreamSplitter*) override;
+    void set_splitter_with_rescan(bool to_server, snort::StreamSplitter* ss, uint32_t seq) override;
+    uint32_t get_paf_position(bool to_server) const override;
     snort::StreamSplitter* get_splitter(bool) override;
 
     void disable_reassembly(snort::Flow*) override;
index ff4d84bed80338c515d8569144c8600c2ca664ce..3488db8de6776caafcaacffb4c144443cb97af5e 100644 (file)
@@ -431,6 +431,20 @@ void TcpStreamTracker::set_splitter(const Flow* flow)
         set_splitter(new AtomSplitter(!client_tracker));
 }
 
+uint32_t TcpStreamTracker::get_paf_position() const
+{
+    if ( reassembler )
+        return reassembler->get_paf_position();
+    return 0;
+}
+
+void TcpStreamTracker::set_splitter_with_rescan(StreamSplitter* ss, uint32_t seq)
+{
+    set_splitter(ss);
+    if ( reassembler )
+        reassembler->reinitialize_paf(seq);
+}
+
 static inline bool both_splitters_aborted(Flow* flow)
 {
     return (flow->get_session_flags() & BOTH_SPLITTERS_YOINKED) == BOTH_SPLITTERS_YOINKED;
index e7bb07b25dc2abb9de1d1bb55dfde6b47b961a40..06c846d2092846c263886e2034761bcd60d3a62e 100644 (file)
@@ -269,6 +269,8 @@ public:
     void init_tcp_state(TcpSession*);
     void set_splitter(snort::StreamSplitter* ss);
     void set_splitter(const snort::Flow* flow);
+    void set_splitter_with_rescan(snort::StreamSplitter* ss, uint32_t seq);
+    uint32_t get_paf_position() const;
 
     snort::StreamSplitter* get_splitter()
     { return splitter; }