├── README.md ├── ja4.irule ├── ja4h.irule ├── ja4l.irule ├── ja4s.irule └── ja4t.irule /README.md: -------------------------------------------------------------------------------- 1 | # F5 iRules for JA4+ Network Fingerprinting 2 | 3 | F5 iRules for generating JA4+ fingerprints. Currently, only JA4, JA4S, JA4T, JA4L, and JA4H fingerprint iRules are provided. More JA4+ fingerprint iRules *MAY* be added in the future. 4 | 5 | > [!WARNING] 6 | >DISCLAIMER: These iRules are provided as-is with no guarantee of performance or functionality. Use at your own risk. 7 | >These iRules have been tested on F5 BIGIPs running TMOS versions 16.1 and 17.1. 8 | 9 | 10 | ## What is JA4+ Network Fingerprinting? 11 | 12 | From the [FoxIO JA4+ Repo](https://github.com/FoxIO-LLC/ja4): 13 | >JA4+ is a suite of network fingerprinting methods that are easy to use and easy to share. These methods are both human >and machine readable to facilitate more effective threat-hunting and analysis. The use-cases for these fingerprints >include scanning for threat actors, malware detection, session hijacking prevention, compliance automation, location >tracking, DDoS detection, grouping of threat actors, reverse shell detection, and many more. 14 | 15 | Please read this blog post for more details: [JA4+ Network Fingerprinting](https://medium.com/foxio/ja4-network-fingerprinting-9376fe9ca637) 16 | 17 | To understand how to read JA4+ fingerprints, see [Technical Details](https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/README.md) 18 | 19 | ## JA4+ Licensing 20 | 21 | > [!IMPORTANT] 22 | >**JA4 TLS Client Fingerprinting is licensed under BSD 3-Clause** 23 | > 24 | >_Copyright (c) 2024, FoxIO_ 25 | >_All rights reserved. 26 | >JA4 TLS Client Fingerprinting is Open-Source, Licensed under BSD 3-Clause. 27 | >For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4_ 28 | > 29 | > 30 | >**All other JA4+ Fingerprints are under the FoxIO License 1.1** 31 | > 32 | >_Copyright (c) 2024, FoxIO, LLC. 33 | >All rights reserved. 34 | >Licensed under FoxIO License 1.1 35 | >For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4_ 36 | 37 | ## How to Use 38 | 39 | **Coming Soon** -------------------------------------------------------------------------------- /ja4.irule: -------------------------------------------------------------------------------- 1 | ############################################################################################### 2 | # iRule to calculate JA4 "Client TLS" 3 | # See JA4 spec on GitHub for more details 4 | # https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md 5 | # 6 | # Copyright (c) 2024, FoxIO 7 | # All rights reserved. 8 | # JA4 TLS Client Fingerprinting is Open-Source, Licensed under BSD 3-Clause 9 | # For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 10 | ############################################################################################### 11 | 12 | proc parseClientHello { payload rlen ja4_ver ja4_tprt } { 13 | 14 | set ja4_sni "i" 15 | ## Define GREASE values so these can be excluded from cipher list 16 | set greaseList "0a0a 1a1a 2a2a 3a3a 4a4a 5a5a 6a6a 7a7a 8a8a 9a9a aaaa baba caca dada eaea fafa" 17 | 18 | ## HEADERS - SKIP: Already captured in XXX_DATA event (record header, handshake header, server version, server random) 19 | set field_offset 43 20 | 21 | ## SESSION ID - SKIP 22 | binary scan ${payload} @${field_offset}c sessID_len 23 | set field_offset [expr {${field_offset} + 1 + ${sessID_len}}] 24 | 25 | ## CLIENT CIPHERS 26 | ## Capture cipher list length and incr offset 27 | binary scan ${payload} @${field_offset}S cipherList_len 28 | set field_offset [expr {${field_offset} + 2}] 29 | 30 | set cipher_offset 0 31 | set cipher_cnt 0 32 | set cipher_list [list] 33 | while { [expr {${cipher_offset} < ${cipherList_len}}] } { 34 | binary scan ${payload} @${field_offset}H4 cipher_hex 35 | if { [lsearch -sorted -inline $greaseList $cipher_hex] eq "" } { 36 | lappend cipher_list ${cipher_hex} 37 | incr cipher_cnt 38 | } 39 | set cipher_offset [expr {${cipher_offset} + 2}] 40 | set field_offset [expr {${field_offset} + 2}] 41 | } 42 | ## Sort cipher_list 43 | set cipher_list [lsort $cipher_list] 44 | ## Convert list to comma-separated string 45 | set cipher_str "" 46 | foreach cipher_hex $cipher_list { 47 | append cipher_str "${cipher_hex}," 48 | } 49 | set cipher_str [string trimright ${cipher_str} ","] 50 | ## Get truncated hash of cipher list string 51 | binary scan [sha256 ${cipher_str}] H* cipher_hash 52 | set trunc_cipher_hash [string range $cipher_hash 0 11] 53 | 54 | ## Format cipher count 55 | if { $cipher_cnt > 99 } { 56 | set cipher_cnt 99 57 | } 58 | set ja4_ccnt [format "%02d" $cipher_cnt] 59 | 60 | ## COMPRESSION METHOD - SKIP 61 | binary scan ${payload} @${field_offset}c compression_len 62 | set field_offset [expr {${field_offset} + 1 + ${compression_len}}] 63 | 64 | 65 | ## EXTENSIONS 66 | set ja4_ecnt 0 67 | set ja4_etype_list [list] 68 | set ja4_alpn "00" 69 | set siga_list "" 70 | 71 | ## Check if there is more data 72 | if { [expr {${field_offset} < ${rlen}}] } { 73 | ## Capture Extensions length and incr offset 74 | binary scan ${payload} @${field_offset}S extList_len 75 | set field_offset [expr {${field_offset} + 2}] 76 | 77 | ## Pad rlen by 1 byte 78 | set rlen [expr ${rlen} + 1] 79 | 80 | ## Parse Extensions 81 | while { [expr {${field_offset} <= ${rlen}}] } { 82 | ## Capture Ext Type, Incr offset past Ext Type 83 | binary scan ${payload} @${field_offset}H4 ext_hex 84 | set field_offset [expr {${field_offset} + 2}] 85 | 86 | ## Capture Ext Length, Incr offset past Ext Length 87 | binary scan ${payload} @${field_offset}S ext_len 88 | set field_offset [expr {${field_offset} + 2}] 89 | 90 | ## Check for GREASE values, if GREASE incr offset by Ext Length 91 | if {[lsearch -sorted -inline $greaseList $ext_hex] ne "" } { 92 | set field_offset [expr {${field_offset} + ${ext_len}}] 93 | continue 94 | } else { 95 | ## Check for specific Extension Types 96 | switch $ext_hex { 97 | "0000" { 98 | ## SNI (00) 99 | ## Set JA4 domain/ip field 100 | set ja4_sni "d" 101 | incr ja4_ecnt 102 | } 103 | "000d" { 104 | ## Signature Algorithms (13) 105 | ## Capture Signature Algorithms length 106 | binary scan ${payload} @${field_offset}S siga_len 107 | set siga_offset 0 108 | while { [expr {${siga_offset} < ${siga_len}}] } { 109 | binary scan ${payload} @[expr {${field_offset} + 2 + ${siga_offset}}]H4 siga_hex 110 | if { [lsearch -sorted -inline $greaseList $siga_hex] eq "" } { 111 | append siga_list "${siga_hex}," 112 | } 113 | incr siga_offset 2 114 | } 115 | set siga_list [string trimright ${siga_list} ","] 116 | lappend ja4_etype_list ${ext_hex} 117 | incr ja4_ecnt 118 | 119 | } 120 | "0010" { 121 | ## ALPN (16) 122 | ## Capture APLN length and First ALPN string length 123 | binary scan ${payload} @${field_offset}Sc alpn_len alpn_str_len 124 | ## Capture the First APLN string value 125 | binary scan ${payload} @[expr {${field_offset} + 3}]a${alpn_str_len} alpn_str 126 | incr ja4_ecnt 127 | } 128 | "0027" { 129 | ## Supported EKT Ciphers (39) 130 | ## Set JA4 Transport Protocol as QUIC 131 | set ja4_tprt "q" 132 | lappend ja4_etype_list ${ext_hex} 133 | incr ja4_ecnt 134 | } 135 | "002b" { 136 | ## Supported Versions (43) 137 | ## Capture Supported Versions length 138 | binary scan ${payload} @${field_offset}c sver_len 139 | set sver_offset 0 140 | set sver_list [list] 141 | while { [expr {${sver_offset} < ${sver_len}}] } { 142 | binary scan ${payload} @[expr {${field_offset} + 1 + ${sver_offset}}]H4 sver_hex 143 | if { [lsearch -sorted -inline $greaseList $sver_hex] eq "" } { 144 | lappend sver_list ${sver_hex} 145 | } 146 | incr sver_offset 2 147 | } 148 | set sver_list [lsort $sver_list] 149 | set ja4_ver [lindex $sver_list end] 150 | lappend ja4_etype_list ${ext_hex} 151 | incr ja4_ecnt 152 | } default { 153 | lappend ja4_etype_list ${ext_hex} 154 | incr ja4_ecnt 155 | } 156 | } 157 | 158 | ## Incr offset past the extension data length. Repeat this loop until we reach rlen (the end of the payload) 159 | set field_offset [expr {${field_offset} + ${ext_len}}] 160 | } 161 | } 162 | } 163 | ## Set JA4 ALPN value 164 | if { [info exist alpn_str] } { 165 | set ja4_alpn "[string index ${alpn_str} 0][string index ${alpn_str} end]" 166 | } 167 | 168 | ## Format extensions count var 169 | if { $ja4_ecnt > 99 } { 170 | set ja4_ecnt 99 171 | } 172 | set ja4_ecnt [format "%02d" $ja4_ecnt] 173 | 174 | ## Sort and format extensions type list 175 | set ja4_etype_list [lsort $ja4_etype_list] 176 | set ja4_etype_str "" 177 | foreach ext_type_hex $ja4_etype_list { 178 | append ja4_etype_str "${ext_type_hex}," 179 | } 180 | set ja4_etype_str [string trimright ${ja4_etype_str} ","] 181 | ## If present, append signature algorithms list to extensions list 182 | if { ${siga_list} ne ""} { 183 | set ja4_etype_str "${ja4_etype_str}_${siga_list}" 184 | } 185 | ## Hash extensions list 186 | binary scan [sha256 ${ja4_etype_str}] H* ja4_ext_hash 187 | set ja4_ext_hash_trunc [string range ${ja4_ext_hash} 0 11] 188 | 189 | ## Format version 190 | switch $ja4_ver { 191 | 0304 { set ja4_ver "13" } 192 | 0303 { set ja4_ver "12" } 193 | 0302 { set ja4_ver "11" } 194 | 0301 { set ja4_ver "10" } 195 | 0300 { set ja4_ver "s3" } 196 | 0200 { set ja4_ver "s2" } 197 | 0100 { set ja4_ver "s1" } 198 | } 199 | 200 | ##Build JA4 string 201 | set ja4_str "${ja4_tprt}${ja4_ver}${ja4_sni}${ja4_ccnt}${ja4_ecnt}${ja4_alpn}_${trunc_cipher_hash}_${ja4_ext_hash_trunc}" 202 | set ja4_r_str "${ja4_tprt}${ja4_ver}${ja4_sni}${ja4_ccnt}${ja4_ecnt}${ja4_alpn}_${cipher_str}_${ja4_etype_str}" 203 | 204 | return "${ja4_str}" 205 | } 206 | 207 | 208 | when CLIENT_ACCEPTED { 209 | unset -nocomplain rlen 210 | set ja4_tprt "t" 211 | set clienthello_payload "" 212 | ## Collect the TCP payload 213 | TCP::collect 214 | } 215 | 216 | when CLIENT_DATA { 217 | 218 | ## Get the TLS packet type and versions 219 | if { ! [info exists rlen] } { 220 | binary scan [TCP::payload] cH4ScH6H4 rtype proto_ver rlen hs_type rilen server_ver 221 | #log local0. "rtype ${rtype} proto_ver ${proto_ver} rlen ${rlen} hs_type ${hs_type} rilen ${rilen} server_ver ${server_ver}" 222 | } 223 | 224 | ## Collect the rest of the record if necessary 225 | if { ( ${rtype} == 22 ) and ( ${hs_type} == 1 ) } { 226 | append clienthello_payload [TCP::payload] 227 | if { [string length $clienthello_payload] < $rlen } { 228 | TCP::release 229 | TCP::collect 230 | return 231 | } else { 232 | set ja4 [call parseClientHello $clienthello_payload ${rlen} ${server_ver} ${ja4_tprt}] 233 | #log local0. "JA4: '${ja4}'" 234 | } 235 | } 236 | 237 | ## Release the payload 238 | TCP::release 239 | } -------------------------------------------------------------------------------- /ja4h.irule: -------------------------------------------------------------------------------- 1 | ############################################################################################### 2 | # iRule to calculate JA4H "HTTP Request" 3 | # See JA4H spec on GitHub for more details 4 | # https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4H.md 5 | # 6 | # Copyright (c) 2024, FoxIO, LLC. 7 | # All rights reserved. 8 | # Licensed under FoxIO License 1.1 9 | # For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 10 | ############################################################################################### 11 | 12 | when CLIENT_ACCEPTED { 13 | # Use these variables to define the -r (raw) and -o (original) switches defined in the JA4 spec 14 | # 0 = false/disabled (default) and 1 = true/enabled 15 | set ja4h_raw 1 16 | set ja4h_original 0 17 | 18 | } 19 | 20 | when HTTP_REQUEST priority 10 { 21 | #Collect JA4H "a" values 22 | set me [string range [string tolower [HTTP::method]] 0 1] 23 | set v [string map {"." ""} [HTTP::version]] 24 | set c "n" 25 | set r "n" 26 | if { [HTTP::header exists "cookie"] } { 27 | set c "c" 28 | } 29 | if { [HTTP::header exists "referer"] } { 30 | set r "r" 31 | } 32 | set lang "0000" 33 | if { [set alval [HTTP::header value "accept-language"]] ne "" } { 34 | if { $alval contains ";" } { 35 | set alval [string range $alval 0 [string first ";" $alval]] 36 | } 37 | set alval [string tolower [string range [string map {"-" ""} ${alval}] 0 3]] 38 | set lang [string replace $lang 0 [string length ${alval}] ${alval}] 39 | } 40 | 41 | #Collect JA4H "b" values 42 | set hc 0 43 | set hstr "" 44 | foreach hname [HTTP::header names] { 45 | if { ${hname} starts_with "X-JA4" } { 46 | continue 47 | } elseif { ([string tolower ${hname}] eq "cookie") || ([string tolower ${hname}] eq "referer")} { 48 | if { ${ja4h_original} }{ 49 | append hstr "${hname}," 50 | } 51 | } else { 52 | incr hc 53 | append hstr "${hname}," 54 | } 55 | } 56 | 57 | if { $hc > 99 } { 58 | set hc 99 59 | } 60 | set hc [format "%02d" $hc] 61 | set hstr [string trimright ${hstr} ","] 62 | binary scan [sha256 ${hstr}] H* hhash 63 | set trunc_hhash [string range $hhash 0 11] 64 | 65 | #Collect JA4H "c" and "d" values 66 | set cstr "" 67 | set clist [list] 68 | set ckvstr "" 69 | #set ckvlist [list] 70 | foreach cname [HTTP::cookie names] { 71 | lappend clist ${cname} 72 | } 73 | if {${ja4h_original} == 0 }{ 74 | set clist [lsort ${clist}] 75 | } 76 | foreach ck ${clist} { 77 | append cstr "${ck}," 78 | append ckvstr "${ck}=[HTTP::cookie value ${ck}]," 79 | } 80 | 81 | set cstr [string trimright ${cstr} ","] 82 | set ckvstr [string trimright ${ckvstr} ","] 83 | if { $c eq "c" } { 84 | binary scan [sha256 ${cstr}] H* chash 85 | binary scan [sha256 ${ckvstr}] H* ckvhash 86 | set trunc_chash [string range $chash 0 11] 87 | set trunc_ckvhash [string range $ckvhash 0 11] 88 | } else { 89 | set trunc_chash "000000000000" 90 | set trunc_ckvhash "000000000000" 91 | } 92 | 93 | # Generate JA4H fingerprint string 94 | set ja4h_fp "${me}${v}${c}${r}${hc}${lang}_${trunc_hhash}_${trunc_chash}_${trunc_ckvhash}" 95 | HTTP::header insert "X-JA4H" $ja4h_fp 96 | 97 | # If enabled, Generate JA4H_r(o) fingerprint string 98 | if { ${ja4h_raw} } { 99 | set ja4hr_fp "${me}${v}${c}${r}${hc}${lang}_${hstr}_${cstr}_${ckvstr}" 100 | set ja4h_xhdr "X-JA4H_r" 101 | if { ${ja4h_original} } { 102 | set ja4h_xhdr "X-JA4H_ro" 103 | } 104 | HTTP::header insert ${ja4h_xhdr} ${ja4hr_fp} 105 | } 106 | } -------------------------------------------------------------------------------- /ja4l.irule: -------------------------------------------------------------------------------- 1 | ############################################################################################### 2 | # iRule to calculate JA4L "light distance" 3 | # See JA4L spec on GitHub for more details 4 | # https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4L.md 5 | # 6 | # Copyright (c) 2024, FoxIO, LLC. 7 | # All rights reserved. 8 | # Licensed under FoxIO License 1.1 9 | # For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 10 | ############################################################################################### 11 | 12 | when FLOW_INIT { 13 | #Set timestamp for initial received SYN 14 | set ja4l_ts1 [clock clicks] 15 | #log local0. "ja4l_ts1: ${ja4l_ts1}" 16 | } 17 | 18 | when CLIENT_ACCEPTED { 19 | #Set timestamp for final received SYN/ACK 20 | set ja4l_ts2 [clock clicks] 21 | #log local0. "ja4l_ts2: ${ja4l_ts2}" 22 | 23 | #Calculate time difference - (ts2-ts1)/2 24 | set ja4l_tcp_latency [expr ($ja4l_ts2 - $ja4l_ts1)/2] 25 | 26 | # Get IP TTL 27 | set ttl [IP::ttl] 28 | 29 | } 30 | 31 | when CLIENTSSL_CLIENTHELLO { 32 | set ja4l_ts3 [clock clicks] 33 | } 34 | 35 | when CLIENTSSL_HANDSHAKE { 36 | set ja4l_ts4 [clock clicks] 37 | # Calculate "application latency" 38 | set ja4l_app_latency [expr ($ja4l_ts4 - $ja4l_ts3)/2] 39 | 40 | # Defone JA4L string 41 | set ja4l "${ja4l_tcp_latency}_${ttl}_${ja4l_app_latency}" 42 | 43 | } 44 | 45 | when HTTP_REQUEST { 46 | if { [info exists ja4l] } { 47 | HTTP::header insert "X-JA4L" "${ja4l}" 48 | } 49 | } -------------------------------------------------------------------------------- /ja4s.irule: -------------------------------------------------------------------------------- 1 | ############################################################################################### 2 | # iRule to calculate JA4S "Server TLS Fingerprint" 3 | # See JA4 spec on GitHub for more details 4 | # https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md 5 | # 6 | # Copyright (c) 2024, FoxIO, LLC. 7 | # All rights reserved. 8 | # Licensed under FoxIO License 1.1 9 | # For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 10 | ############################################################################################### 11 | 12 | proc parseServerHello { payload rlen ja4s_ver ja4s_tprt } { 13 | 14 | ## HEADERS - SKIP: Already captured in XXX_DATA event (record header, handshake header, server version, server random) 15 | set field_offset 43 16 | 17 | ## SESSION ID - SKIP 18 | binary scan ${payload} @${field_offset}c sessID_len 19 | set field_offset [expr {${field_offset} + 1 + ${sessID_len}}] 20 | 21 | ## SERVER CIPHER 22 | binary scan ${payload} @${field_offset}H4 ja4s_cipher 23 | set field_offset [expr ${field_offset} + 2] 24 | 25 | ## COMPRESSION METHOD - SKIP 26 | set field_offset [expr {${field_offset} + 1}] 27 | 28 | ## EXTENSIONS 29 | set ja4s_ecnt 0 30 | set ja4s_etype_list [list] 31 | set ja4s_alpn "00" 32 | 33 | if { [expr {${field_offset} < ${rlen}}] } { 34 | ## Extensions length - SKIP 35 | set field_offset [expr {${field_offset} + 2}] 36 | 37 | ## Pad rlen by 1 byte 38 | set rlen [expr ${rlen} + 1] 39 | 40 | ## Parse Extensions 41 | while { [expr {${field_offset} <= ${rlen}}] } { 42 | ## Capture Ext Type, Append to Ext List, Incr Ext Count, Incr offset past Ext Type 43 | binary scan ${payload} @${field_offset}H4 ext 44 | lappend ja4s_etype_list ${ext} 45 | incr ja4s_ecnt 46 | set field_offset [expr {${field_offset} + 2}] 47 | 48 | ## Capture Ext Length, Incr offset past Ext Length 49 | binary scan ${payload} @${field_offset}S ext_len 50 | set field_offset [expr {${field_offset} + 2}] 51 | 52 | ## Check for specific Extension Types 53 | switch $ext { 54 | "0010" { 55 | ## ALPN (16) 56 | ## Capture APLN length and ALPN string length 57 | binary scan ${payload} @${field_offset}Sc alpn_len alpn_str_len 58 | ## Capture the APLN string value 59 | binary scan ${payload} @[expr {${field_offset} + 3}]a${alpn_str_len} alpn_str 60 | } 61 | "0027" { 62 | ## Supported EKT Ciphers (39) 63 | ## Set JA4S Transport Protocol as QUIC 64 | set ja4s_tprt "q" 65 | } 66 | "002b" { 67 | ## Supported Versions (43) 68 | ## Capture Server-Selected Supported Version 69 | binary scan ${payload} @[expr {${field_offset} + 2}]H[expr {(${ext_len} - 2) * 2}] ja4s_ver 70 | } 71 | } 72 | 73 | #log local0. "SERVER_HELLO - EXT: ${ext} LEN: ${ext_len} VAL: ${ext_data}" 74 | 75 | ## Incr offset past the extension data length. Repeat this loop until we reach rlen (the end of the payload) 76 | set field_offset [expr {${field_offset} + ${ext_len}}] 77 | } 78 | } 79 | 80 | if { [info exist alpn_str] } { 81 | set ja4s_alpn "[string index ${alpn_str} 0][string index ${alpn_str} end]" 82 | } 83 | 84 | ## Format extensions count var 85 | if { $ja4s_ecnt > 99 } { 86 | #log local0. "Ext count is > 99, setting to 99" 87 | set ja4s_ecnt 99 88 | } 89 | set ja4s_ecnt [format "%02d" $ja4s_ecnt] 90 | 91 | ## Sort and format extensions type list 92 | set ja4s_etype_list [lsort $ja4s_etype_list] 93 | set ja4s_etype_str "" 94 | foreach ext_type_hex $ja4s_etype_list { 95 | append ja4s_etype_str "${ext_type_hex}," 96 | } 97 | set ja4s_etype_str [string trimright ${ja4s_etype_str} ","] 98 | binary scan [sha256 ${ja4s_etype_str}] H* ja4s_ext_hash 99 | set ja4s_ext_hash_trunc [string range ${ja4s_ext_hash} 0 11] 100 | 101 | ## Format version 102 | switch $ja4s_ver { 103 | 0304 { set ja4s_ver "13" } 104 | 0303 { set ja4s_ver "12" } 105 | 0302 { set ja4s_ver "11" } 106 | 0301 { set ja4s_ver "10" } 107 | 0300 { set ja4s_ver "s3" } 108 | 0200 { set ja4s_ver "s2" } 109 | 0100 { set ja4s_ver "s1" } 110 | } 111 | 112 | #JA4S Algorithm: 113 | #(q or t) 114 | #(2 character tls version) 115 | #(2 character number of extensions) 116 | #(first and last character of the ALPN chosen) 117 | #_ 118 | #(cipher suite chosen in hex) 119 | #_ 120 | #(truncated sha256 hash of the extensions in the order that they appear) 121 | 122 | set ja4s_str "${ja4s_tprt}${ja4s_ver}${ja4s_ecnt}${ja4s_alpn}_${ja4s_cipher}_${ja4s_ext_hash_trunc}" 123 | 124 | return "${ja4s_str}" 125 | } 126 | 127 | 128 | when SERVER_CONNECTED { 129 | unset -nocomplain rlen 130 | set ja4s_tprt "t" 131 | ## Collect the TCP payload 132 | TCP::collect 133 | } 134 | 135 | when SERVER_DATA { 136 | 137 | ## Get the TLS packet type and versions 138 | if { ! [info exists rlen] } { 139 | binary scan [TCP::payload] cH4ScH6H4 rtype proto_ver rlen hs_type rilen server_ver 140 | log local0. "rtype ${rtype} proto_ver ${proto_ver} rlen ${rlen} hs_type ${hs_type} rilen ${rilen} server_ver ${server_ver}" 141 | 142 | if { ( ${rtype} == 22 ) and ( ${hs_type} == 2 ) } { 143 | log local0. "Found SERVER_HELLO" 144 | set ja4s [call parseServerHello [TCP::payload] ${rlen} ${server_ver} ${ja4s_tprt}] 145 | log local0. "JA4S: '${ja4s}'" 146 | 147 | } 148 | } 149 | 150 | # Collect the rest of the record if necessary 151 | if { [TCP::payload length] < $rlen } { 152 | TCP::collect $rlen 153 | } 154 | 155 | ## Release the payload 156 | TCP::release 157 | } -------------------------------------------------------------------------------- /ja4t.irule: -------------------------------------------------------------------------------- 1 | ############################################################################################### 2 | # iRule to calculate JA4T "TCP Fingerprint" 3 | # See JA4 spec on GitHub for more details 4 | # https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md 5 | # 6 | # Copyright (c) 2024, FoxIO, LLC. 7 | # All rights reserved. 8 | # Licensed under FoxIO License 1.1 9 | # For full license text and more details, see the repo root https://github.com/FoxIO-LLC/ja4 10 | ############################################################################################### 11 | 12 | when FLOW_INIT { 13 | # To block based on a list of "bad" JA4T values, set "ja4t_blocking" to 1 (one) and 14 | # set the name of your JA4T blocklist data group 15 | set ja4t_blocking 0 16 | set ja4t_blocklist "dg_ja4t_blocklist" 17 | set ja4t_log_blocks 1 18 | 19 | 20 | 21 | set recv_win [DATAGRAM::tcp window] 22 | set ops_list "" 23 | foreach option [DATAGRAM::tcp option] { 24 | scan ${option} "%s %\[^-\]" kind value 25 | switch ${kind} { 26 | "2" { 27 | binary scan ${value} S* mss 28 | } 29 | "3" { 30 | binary scan ${value} c* win_scale 31 | regexp {(?:123 )?(.*?)(?: 125)?} ${win_scale} match win_scale 32 | } 33 | } 34 | append ops_list "${kind}-" 35 | unset -nocomplain kind 36 | unset -nocomplain value 37 | } 38 | set ops_list [string trimright ${ops_list} "-"] 39 | if { ${ops_list} eq "" } { 40 | set ops_list "00" 41 | } 42 | if {![info exists win_scale]}{ 43 | set win_scale "00" 44 | } 45 | if {![info exists mss]}{ 46 | set mss "00" 47 | } 48 | 49 | set ja4t "${recv_win}_${ops_list}_${mss}_${win_scale}" 50 | 51 | if { ${ja4t_blocking} } { 52 | if { [class exists ${ja4t_blocklist}] } { 53 | if { [set blocked_ja4t [class match -element ${ja4t} equals ${ja4t_blocklist}]] ne "" } { 54 | if { ${ja4t_log_blocks} } { log local0. "Dropping connection from JA4T: '${blocked_ja4t}'" } 55 | drop 56 | } 57 | } else { 58 | log local0. "JA4T blocking is enabled but data group '${ja4t_blocklist}' does not exist. Create data group to enable blocking." 59 | } 60 | } 61 | } 62 | 63 | when HTTP_REQUEST { 64 | if {[info exists ja4t]}{ 65 | HTTP::header insert "X-JA4T" ${ja4t} 66 | } 67 | } --------------------------------------------------------------------------------