From 97ec7c4ebd4a2c032898d100a4563c8b7bee93af Mon Sep 17 00:00:00 2001 From: "brendan.pham" Date: Tue, 17 Mar 2026 11:05:32 +0700 Subject: [PATCH] feat: update f5 v5.0.0 template --- rules/netacea_mitigate.tcl | 206 +++++++++++++++++++++++++++---------- src/index.ts | 1 + 2 files changed, 152 insertions(+), 55 deletions(-) diff --git a/rules/netacea_mitigate.tcl b/rules/netacea_mitigate.tcl index 1648b2a..3117b0a 100644 --- a/rules/netacea_mitigate.tcl +++ b/rules/netacea_mitigate.tcl @@ -1,3 +1,13 @@ +when RULE_INIT { + # Initialize header policy variables. + # Actual fetch deferred to first HTTP_REQUEST (ILX::init not available in RULE_INIT). + set static::mitigation_headers {} + set static::mitigation_headers_loaded 0 + set static::mitigation_headers_fetch_attempts 0 + set static::mitigation_headers_max_attempts 3 + log local0.debug "Netacea RULE_INIT: iRule loaded (v2 with header policy support)" +} + when HTTP_REQUEST { set http_request_time [clock clicks -milliseconds] if {[catch {ILX::init "Netacea" "netacea"} handle]} { @@ -7,6 +17,32 @@ when HTTP_REQUEST { return } + # Fetch header policy on first request (ILX::init not available in RULE_INIT) + if { !$static::mitigation_headers_loaded } { + if { $static::mitigation_headers_fetch_attempts < $static::mitigation_headers_max_attempts } { + incr static::mitigation_headers_fetch_attempts + if {[catch {ILX::call $handle getMitigationHeaderPolicy} policyResult]} { + log local0.error "Client - [IP::client_addr], ILX failure: could not fetch mitigation header policy (attempt $static::mitigation_headers_fetch_attempts): $policyResult" + # After max attempts, stop retrying - requests proceed with empty header policy + if { $static::mitigation_headers_fetch_attempts >= $static::mitigation_headers_max_attempts } { + log local0.warn "Client - [IP::client_addr], Netacea policy: max attempts reached, proceeding with empty header policy" + set static::mitigation_headers_loaded 1 + } + } else { + log local0.debug "Client - [IP::client_addr], Netacea policy: getMitigationHeaderPolicy returned: $policyResult" + # Guard against empty policy reply: split "" "," returns {""} + set splitResult [split $policyResult ","] + if { [llength $splitResult] == 1 && [lindex $splitResult 0] eq "" } { + log local0.warn "Client - [IP::client_addr], Netacea policy: empty policy reply from worker" + } else { + set static::mitigation_headers $splitResult + } + set static::mitigation_headers_loaded 1 + log local0.debug "Client - [IP::client_addr], Netacea policy: loaded [llength $static::mitigation_headers] headers: $static::mitigation_headers" + } + } + } + set method [HTTP::method] set uri [HTTP::uri] set useragent [HTTP::header value "User-Agent"] @@ -14,11 +50,14 @@ when HTTP_REQUEST { # strip route domain from ip set clientaddress [regsub {%.*} [IP::client_addr] ""] + # Collect header names for worker to compute a fingerprint hash + set headerNames [join [HTTP::header names] ","] + # Handle Captcha if { [HTTP::path] equals "/AtaVerifyCaptcha" && [string tolower $method] eq "post"} { # Check if there is a Content-Length header if { [HTTP::header exists "Content-Length"] } { - if { [HTTP::header "Content-Length"] > 64000 }{ + if { [HTTP::header "Content-Length"] > 64000 } { # Content-Length over 64kb so collect 64kb set content_length 64000 } else { @@ -34,103 +73,160 @@ when HTTP_REQUEST { HTTP::collect $content_length } } else { # Check reputation - # set result [ILX::call $handle check $clientaddress $useragent [HTTP::method] [HTTP::uri] [HTTP::cookie value "_mitata"] [HTTP::cookie value "_mitatacaptcha"]] - if {[catch {ILX::call $handle handleRequest $clientaddress $useragent $method [HTTP::path] [HTTP::header value "cookie"]} result]} { + # Collect header values based on policy — each value individually base64-encoded + set headerValuesList {} + foreach hdr $static::mitigation_headers { + lappend headerValuesList [b64encode [HTTP::header value $hdr]] + } + set headerValues [join $headerValuesList ","] + + if {[catch {ILX::call $handle handleRequest $clientaddress $method [HTTP::uri] "" $headerNames $headerValues} result]} { log local0.error "Client - $clientaddress, ILX failure: could not reach mitigate API: $result" - set result {"" 0 [] "" false "" []} # Send user graceful error message, then exit event return } - if {[info exists result] && [llength $result] == 7}{ - set body [ lindex $result 0 ] - set apiCallStatus [ lindex $result 1 ] - set cookies [ lindex $result 2 ] - set sessionStatus [ lindex $result 3 ] - set mitigated [ lindex $result 4 ] - set mitata [ lindex $result 5 ] - set injectHeaders [ lindex $result 6] - set ilx_request_time [clock clicks -milliseconds] - set HTTP::mitata $mitata - set HTTP::sessionStatus $sessionStatus + if {[info exists result] && [llength $result] >= 8} { + set body [lindex $result 0] + set apiCallStatus [lindex $result 1] + set sessionStatus [lindex $result 2] + set mitigated [lindex $result 3] + set mitata [lindex $result 4] + set headerFingerprint [lindex $result 5] + set requestHeaders [lindex $result 6] + set responseHeaders [lindex $result 7] + + # Set inject headers on request (dynamic — no hardcoded names) + foreach {hname hval} $requestHeaders { + HTTP::header insert $hname [b64decode $hval] + } if { $mitigated } then { - # calcluate request time + # calculate request time set http_response_time [clock clicks -milliseconds] - set request_time [ expr {$http_response_time - $http_request_time} ] - ILX::call $handle ingest $clientaddress $useragent 403 $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus - HTTP::respond 403 content $body Set-Cookie [join $cookies "; "] - } - if { [llength $injectHeaders] == 3} { - HTTP::header insert "x-netacea-match" [ lindex $injectHeaders 0 ] - HTTP::header insert "x-netacea-mitigate" [ lindex $injectHeaders 1 ] - HTTP::header insert "x-netacea-captcha" [ lindex $injectHeaders 2 ] - HTTP::header insert "x-netacea-event-id" [ lindex $injectHeaders 3 ] + set request_time [expr {$http_response_time - $http_request_time}] + log local0.debug "Client - $clientaddress, Netacea mitigate: MITIGATED 403, request_time=${request_time}ms" + ILX::call $handle ingest $clientaddress $useragent 403 $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus $headerFingerprint + + # Build HTTP::respond dynamically from responseHeaders + set respondArgs [list HTTP::respond 403 content $body] + foreach {hname hval} $responseHeaders { + lappend respondArgs $hname [b64decode $hval] + } + eval $respondArgs } + } else { + log local0.warn "Client - $clientaddress, Netacea mitigate: unexpected result length [llength $result], expected >= 8" } } } when HTTP_REQUEST_DATA { - if { [HTTP::path] equals "/AtaVerifyCaptcha" && [string tolower $method] eq "post"} { # Handle Captcha tests + if {![info exists handle]} { return } + + if { [HTTP::path] equals "/AtaVerifyCaptcha" && [string tolower [HTTP::method]] eq "post"} { set method [HTTP::method] set uri [HTTP::uri] set useragent [HTTP::header value "User-Agent"] + set referer [HTTP::header value "referer"] # strip route domain from ip set clientaddress [regsub {%.*} [IP::client_addr] ""] - if {[catch {ILX::call $handle handleRequest $clientaddress $useragent $method [HTTP::path] [HTTP::header value "cookie"] [HTTP::payload]} result]} { + if {![info exists http_request_time]} { + set http_request_time [clock clicks -milliseconds] + } + + # Collected again because HTTP_REQUEST_DATA is a separate event + set headerNames [join [HTTP::header names] ","] + + # Guard mitigation headers policy + if { !$static::mitigation_headers_loaded || [llength $static::mitigation_headers] == 0 } { + log local0.warn "Client - $clientaddress, Netacea captcha: mitigation headers not loaded, proceeding with empty header values" + } + + # Collect header values based on policy — each value individually base64-encoded + set headerValuesList {} + foreach hdr $static::mitigation_headers { + lappend headerValuesList [b64encode [HTTP::header value $hdr]] + } + set headerValues [join $headerValuesList ","] + + if {[catch {ILX::call $handle handleRequest $clientaddress $method [HTTP::path] [HTTP::payload] $headerNames $headerValues} result]} { log local0.error "Client - $clientaddress, ILX failure: could not handle captcha test" # Send user graceful error message, then exit event return } - set body [ lindex $result 0 ] - set apiCallStatus [ lindex $result 1 ] - set cookies [ lindex $result 2 ] - set sessionStatus [ lindex $result 3 ] - set mitigated [ lindex $result 4 ] - set mitata [ lindex $result 5 ] - set injectHeaders [ lindex $result 6] - foreach cookie $cookies { - HTTP::header insert "Set-Cookie" $cookie + + if {[llength $result] < 8} { + log local0.warn "Client - $clientaddress, Netacea captcha: unexpected result length [llength $result], expected >= 8" + return } - # calcuate request time + set body [lindex $result 0] + set apiCallStatus [lindex $result 1] + set sessionStatus [lindex $result 2] + set mitigated [lindex $result 3] + set mitata [lindex $result 4] + set headerFingerprint [lindex $result 5] + set requestHeaders [lindex $result 6] + set responseHeaders [lindex $result 7] + + log local0.debug "Client - $clientaddress, Netacea captcha: sessionStatus=$sessionStatus mitigated=$mitigated fingerprint=$headerFingerprint" + set http_response_time [clock clicks -milliseconds] - set request_time [ expr {$http_response_time - $http_request_time} ] - # ILX::call $handle ingest $clientaddress $useragent 403 $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus - if {[catch {ILX::call $handle ingest $clientaddress $useragent 403 $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus} result]} { - log local0.error "Client - $clientaddress, ILX failure: could not reach ingest API for passed captcha" - # Send user graceful error message, then exit event - return + set request_time [expr {$http_response_time - $http_request_time}] + if {[catch {ILX::call $handle ingest $clientaddress $useragent 403 $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus $headerFingerprint} ingestResult]} { + log local0.error "Client - $clientaddress, ILX failure: could not reach ingest API for captcha" } - HTTP::respond 403 content $body Set-Cookie [lindex $cookies 0] Set-Cookie [lindex $cookies 1] + # Build HTTP::respond dynamically + set respondArgs [list HTTP::respond 403 content $body] + foreach {hname hval} $responseHeaders { + lappend respondArgs $hname [b64decode $hval] + } + eval $respondArgs } } when HTTP_RESPONSE { - # Put set-cookie header together and get mitata cookie value - if {[info exists cookies]}{ - foreach cookie $cookies { - HTTP::header insert "Set-Cookie" $cookie + # Set response headers from worker reply (e.g. Set-Cookie) + if {[info exists responseHeaders]} { + foreach {hname hval} $responseHeaders { + HTTP::header insert $hname [b64decode $hval] } } set clientaddress [regsub {%.*} [IP::client_addr] ""] - # calcluate request time + # Guard handle — if ILX::init failed in HTTP_REQUEST, skip ingest + if {![info exists handle]} { + return + } + + # Guard http_request_time + if {![info exists http_request_time]} { + set http_request_time [clock clicks -milliseconds] + } + + # Guard cross-event variables with re-computation + if {![info exists method]} { set method [HTTP::method] } + if {![info exists uri]} { set uri [HTTP::uri] } + if {![info exists useragent]} { set useragent [HTTP::header value "User-Agent"] } + if {![info exists referer]} { set referer [HTTP::header value "referer"] } + + # calculate request time set http_response_time [clock clicks -milliseconds] - set request_time [ expr {$http_response_time - $http_request_time} ] + set request_time [expr {$http_response_time - $http_request_time}] - if {[info exists result] && [llength $result] == 7} { - set sessionStatus [ lindex $result 3 ] - set mitata [ lindex $result 5 ] + if {[info exists result] && [llength $result] >= 8} { + set sessionStatus [lindex $result 2] + set mitata [lindex $result 4] + set headerFingerprint [lindex $result 5] } else { set sessionStatus "" set mitata "" + set headerFingerprint "" } - # ILX::call $handle ingest $clientaddress $useragent [HTTP::status] $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus - if {[catch {ILX::call $handle ingest $clientaddress $useragent [HTTP::status] $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus} result]} { + if {[catch {ILX::call $handle ingest $clientaddress $useragent [HTTP::status] $method $uri "http" $referer [HTTP::header value "Content-Length"] $request_time $mitata $sessionStatus $headerFingerprint} result]} { log local0.error "Client - $clientaddress, ILX failure: could not reach ingest API" # Send user graceful error message, then exit event return diff --git a/src/index.ts b/src/index.ts index 38a4846..3274ba9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ const f5 = require('f5-nodejs') const ilx: IlxServer = new f5.ILXServer() const netacea = new NetaceaF5(NetaceaConfig as F5ConstructorArgs) +netacea.registerPolicyHandler(ilx) netacea.registerMitigateHandler(ilx) netacea.registerIngestHandler(ilx)