irules
20644 TopicsAPM-DHCP Access Policy Example and Detailed Instructions
Prepared with Mark Quevedo, F5 Principal Software Engineer May, 2020 Sectional Navigation links Important Version Notes || Installation Guide || What Is Going On Here? || Parameters You Set In Your APM Access Policy || Results of DHCP Request You Use in Access Policy || Compatibility Tips and Troubleshooting Introduction Ordinarily you assign an IP address to the “inside end” of an APM Network Tunnel (full VPN connection) from an address Lease Pool, from a static list, or from an LDAP or RADIUS attribute. However, you may wish to assign an IP address you get from a DHCP server. Perhaps the DHCP server manages all available client addresses. Perhaps it handles dynamic DNS for named client workstations. Or perhaps the DHCP server assigns certain users specific IP addresses (for security filtering). Your DHCP server may even assign client DNS settings as well as IP addresses. APM lacks DHCP address assignment support (though f5's old Firepass VPN had it ). We will use f5 iRules to enable DHCP with APM. We will send data from APM session variables to the DHCP server so it can issue the “right” IP address to each VPN tunnel based on user identity, client info, etc. Important Version Notes Version v4c includes important improvements and bug fixes. If you are using an older version, you should upgrade. Just import the template with “Overwrite existing templates” checked, then “reconfigure” your APM-DHCP Application Service—you can simply click “Finished” without changing any options to update the iRules in place. Installation Guide First install the APM-DHCP iApp template (file DHCP_for_APM.tmpl). Create a new Application Service as shown (choose any name you wish). Use the iApp to manage the APM-DHCP virtual servers you need. (The iApp will also install necessary iRules.) You must define at least one APM-DHCP virtual server to receive and send DHCP packets. Usually an APM-DHCP virtual server needs an IP address on the subnet on which you expect your DHCP server(s) to assign client addresses. You may define additional APM-DHCP virtual servers to request IP addresses on additional subnets from DHCP. However, if your DHCP server(s) support subnet-selection (see session.dhcp.subnet below) then you may only need a single APM-DHCP virtual server and it may use any IP that can talk to your DHCP server(s). It is best to give each APM-DHCP virtual server a unique IP address but you may use an BIG-IP Self IP as per SOL13896 . Ensure your APM and APM-DHCP virtual servers are in the same TMOS Traffic Group (if that is impossible set TMOS db key tmm.sessiondb.match_ha_unit to false). Ensure that your APM-DHCP virtual server(s) and DHCP server(s) or relay(s) are reachable via the same BIG-IP route domain. Specify in your IP addresses any non-zero route-domains you are using (e.g., “192.168.0.20%3”)—this is essential. (It is not mandatory to put your DHCP-related Access Policy Items into a Macro—but doing so makes the below screenshot less wide!) Into your APM Access Policy, following your Logon Page and AD Auth (or XYZ Auth) Items (etc.) but before any (Full/Advanced/simple) Resource Assign Item which assigns the Network Access Resource (VPN), insert both Machine Info and Windows Info Items. (The Windows Info Item will not bother non-Windows clients.) Next insert a Variable Assign Item and name it “DHCP Setup”. In your “DHCP Setup” Item, set any DHCP parameters (explained below) that you need as custom session variables. You must set session.dhcp.servers. You must also set session.dhcp.virtIP to the IP address of an APM-DHCP virtual server (either here or at some point before the “DHCP_Req” iRule Event Item). Finally, insert an iRule Event Item (name it “DHCP Req”) and set its Agent ID to DHCP_req. Give it a Branch Rule “Got IP” using the expression “expr {[mcget {session.dhcp.address}] ne ""}” as illustrated. You must attach iRule ir-apm-policy-dhcp to your APM virtual server (the virtual server to which your clients connect). Neither the Machine Info Item nor the Windows Info Item is mandatory. However, each gathers data which common DHCP servers want to see. By default DHCP_req will send that data, when available, to your DHCP servers. See below for advanced options: DHCP protocol settings, data sent to DHCP server(s), etc. Typically your requests will include a user identifier from session.dhcp.subscriber_ID and client (machine or connection) identifiers from other parameters. The client IP address assigned by DHCP will appear in session.dhcp.address. By default, the DHCP_req iRule Event handler will also copy that IP address into session.requested.clientip where the Network Access Resource will find it. You may override that behavior by setting session.dhcp.copy2var (see below). Any “vendor-specific information” supplied by the DHCP server 1 (keyed by the value of session.dhcp.vendor_class) will appear in variables session.dhcp.vinfo.N where N is a tag number (1-254). You may assign meanings to tag numbers. Any DNS parameters the DHCP server supplies 2 are in session.dhcp.dns_servers and session.dhcp.dns_suffix. If you want clients to use those DNS server(s) and/or DNS default search domain, put the name of every Network Access Resource your Access Policy may assign to the client into the session.dhcp.dns_na_list option. NB: this solution does not renew DHCP address leases automatically, but it does release IP addresses obtained from DHCP after APM access sessions terminate. 3 Please configure your DHCP server(s) for an address lease time longer than your APM Maximum Session Timeout. Do not configure APM-DHCP virtual servers in different BIG-IP route domains so they share any part of a DHCP client IP range (address lease pool). For example, do not use two different APM-DHCP virtual servers 10.1.5.2%6 and 10.1.5.2%8 with one DHCP client IP range 10.1.5.10—10.1.5.250. APM-DHCP won’t recognize when two VPN sessions in different route domains get the same client IP from a non-route-domain-aware DHCP server, so it may not release their IP’s in proper sequence. This solution releases DHCP address leases for terminated APM sessions every once in a while, when a new connection comes in to the APM virtual server (because the BIG IP only executes the relevant iRules on the “event” of each new connection). When traffic is sparse (say, in the middle of the night) there may be some delay in releasing addresses for dead sessions. If ever you think this solution isn’t working properly, be sure to check the BIG IP’s LTM log for warning and error messages. DHCP Setup (a Variable Assign Item) will look like: Put the IP of (one of) your APM-DHCP virtual server(s) in session.dhcp.virtIP. Your DHCP server list may contain addresses of DHCP servers or relays. You may list a directed broadcast address (e.g., “172.16.11.255”) instead of server addresses but that will generate extra network chatter. To log information about DHCP processing for the current APM session you may set variable session.dhcp.debug to true (don’t leave it enabled when not debugging). DHCP Req (an iRule Event Item) will look like: Note DHCP Req branch rules: If DHCP fails, you may wish to warn the user: (It is not mandatory to Deny access after DHCP failure—you may substitute another address into session.requested.clientip or let the Network Access Resource use a Lease Pool.) What is going on here? We may send out DHCP request packets easily enough using iRules’ SIDEBAND functions, but it is difficult to collect DHCP replies using SIDEBAND. 4 Instead, we must set up a distinct LTM virtual server to receive DHCP replies on UDP port 67 at a fixed address. We tell the DHCP server(s) we are a DHCP relay device so replies will come back to us directly (no broadcasting). 5 For a nice explanation of the DHCP request process see http://technet.microsoft.com/en-us/library/cc940466.aspx. At this time, we support only IPv4, though adding IPv6 would require only toil, not genius. By default, a DHCP server will assign a client IP on the subnet where the DHCP relay device (that is, your APM-DHCP virtual server) is homed. For example, if your APM-DHCP virtual server’s address were 172.30.4.2/22 the DHCP server would typically lease out a client IP on subnet 172.30.4.0. Moreover, the DHCP server will communicate directly with the relay-device IP so appropriate routes must exist and firewall rules must permit. If you expect to assign client IP’s to APM tunnel endpoints on multiple subnets you may need multiple APM-DHCP virtual servers (one per subnet). Alternatively, some but not all DHCP servers 6 support the rfc3011 “subnet selection” or rfc3527 “subnet/link-selection sub-option” so you can request a client IP on a specified subnet using a single APM-DHCP virtual server (relay device) IP which is not homed on the target subnet but which can communicate easily with the DHCP server(s): see parameter session.dhcp.subnet below. NOTE: The subnet(s) on which APM Network Access (VPN) tunnels are homed need not exist on any actual VLAN so long as routes to any such subnet(s) lead to your APM (BIG-IP) device. Suppose you wish to support 1000 simultaneous VPN connections and most of your corporate subnets are /24’s—but you don’t want to set up four subnets for VPN users. You could define a virtual subnet—say, 172.30.4.0/22—tell your DHCP server(s) to assign addresses from 172.30.4.3 thru 172.30.7.254 to clients, put an APM-DHCP virtual server on 172.30.4.2, and so long as your Layer-3 network knows that your APM BIG-IP is the gateway to 172.30.4.0/22, you’re golden. When an APM Access Policy wants an IP address from DHCP, it will first set some parameters into APM session variables (especially the IP address(es) of one or more DHCP server(s)) using a Variable Assign Item, then use an iRule Event Item to invoke iRule Agent DHCP_req in ir apm policy dhcp. DHCP_req will send DHCPDISCOVERY packets to the specified DHCP server(s). The DHCP server(s) will reply to those packets via the APM-DHCP virtual-server, to which iRule ir apm dhcp must be attached. That iRule will finish the 4-packet DHCP handshake to lease an IP address. DHCP_req handles timeouts/retransmissions and copies the client IP address assigned by the DHCP server into APM session variables for the Access Policy to use. We use the APM Session-ID as the DHCP transaction-ID XID and also (by default) in the value of chaddr to avert collisions and facilitate log tracing. Parameters You Set In Your APM Access Policy Required Parameters session.dhcp.virtIP IP address of an APM-DHCP virtual-server (on UDP port 67) with iRule ir-apm-dhcp. This IP must be reachable from your DHCP server(s). A DHCP server will usually assign a client IP on the same subnet as this IP, though you may be able to override that by setting session.dhcp.subnet. You may create APM-DHCP virtual servers on different subnets, then set session.dhcp.virtIP in your Access Policy (or branch) to any one of them as a way to request a client IP on a particular subnet. No default. Examples (“Custom Expression” format): expr {"172.16.10.245"} or expr {"192.0.2.7%15"} session.dhcp.servers A TCL list of one or more IP addresses for DHCP servers (or DHCP relays, such as a nearby IP router). When requesting a client IP address, DHCP packets will be sent to every server on this list. NB: IP broadcast addresses like 10.0.7.255 may be specified but it is better to list specific servers (or relays). Default: none. Examples (“Custom Expression” format): expr {[list "10.0.5.20" "10.0.7.20"]} or expr {[list "172.30.1.20%5"]} Optional Parameters (including some DHCP Options) NOTE: when you leave a parameter undefined or empty, a suitable value from the APM session environment may be substituted (see details below). The defaults produce good results in most cases. Unless otherwise noted, set parameters as Text values. To exclude a parameter entirely set its Text value to '' [two ASCII single-quotes] (equivalent to Custom Expression return {''} ). White-space and single-quotes are trimmed from the ends of parameter values, so '' indicates a nil value. It is best to put “Machine Info” and “Windows Info” Items into your Access Policy ahead of your iRule Event “DHCP_req” Item (Windows Info is not available for Mac clients beginning at version 15.1.5 as they are no longer considered safe). session.dhcp.debug Set to 1 or “true” to log DHCP-processing details for the current APM session. Default: false. session.dhcp.firepass Leave this undefined or empty (or set to “false”) to use APM defaults (better in nearly all cases). Set to “true” to activate “Firepass mode” which alters the default values of several other options to make DHCP messages from this Access Policy resemble messages from the old F5 Firepass product. session.dhcp.copy2var Leave this undefined or empty (the default) and the client IP address from DHCP will be copied into the Access Policy session variable session.requested.clientip, thereby setting the Network Access (VPN) tunnel’s inside IP address. To override the default, name another session variable here or set this to (Text) '' to avert copying the IP address to any variable. session.dhcp.dns_na_list To set the client's DNS server(s) and/or DNS default search domain from DHCP, put here a Custom Expression TCL list of the name(s) of the Network Access Resource(s) you may assign to the client session. Default: none. Example: expr {[list "/Common/NA" "/Common/alt-NA"]} session.dhcp.broadcast Set to “true” to set the DHCP broadcast flag (you almost certainly should not use this). session.dhcp.vendor_class Option 60 A short string (32 characters max) identifying your VPN server. Default: “f5 APM”. Based on this value the DHCP server may send data to session.dhcp.vinfo.N (see below). session.dhcp.user_class Option 77 A Custom Expression TCL list of strings by which the DHCP server may recognize the class of the client device (e.g., “kiosk”). Default: none (do not put '' here). Example: expr {[list "mobile" "tablet"]} session.dhcp.client_ID Option 61 A unique identifier for the remote client device. Microsoft Windows DHCP servers expect a representation of the MAC address of the client's primary NIC. If left undefined or empty the primary MAC address discovered by the Access Policy Machine Info Item (if any) will be used. If no value is set and no Machine Info is available then no client_ID will be sent and the DHCP server will distinguish clients by APM-assigned ephemeral addresses (in session.dhcp.hwcode). If you supply a client_ID value you may specify a special code, a MAC address, a binary string, or a text string. Set the special code “NONE” (or '') to avoid sending any client_ID, whether Machine Info is available or not. Set the special code “XIDMAC” to send a unique MAC address for each APM VPN session—that will satisfy DHCP servers desiring client_ID‘s while averting IP collisions due to conflicting Machine Info MAC’s like Apple Mac Pro’s sometimes provide. A value containing twelve hexadecimal digits, possibly separated by hyphens or colons into six groups of two or by periods into three groups of four, will be encoded as a MAC address. Values consisting only of hexadecimal digits, of any length other than twelve hexits, will be encoded as a binary string. A value which contains chars other than [0-9A-Fa-f] and doesn't seem to be a MAC address will be encoded as a text string. You may enclose a text string in ASCII single-quotes (') to avert interpretation as hex/binary (the quotes are not part of the text value). On the wire, MAC-addresses and text-strings will be prefixed by type codes 0x01 and 0x00 respectively; if you specify a binary string (in hex format) you must include any needed codes. Default: client MAC from Machine Info, otherwise none. Example (Text value): “08-00-2b-2e-d8-5e”. session.dhcp.hostname Option 12 A hostname for the client. If left undefined or empty, the short computer name discovered by the APM Access Policy Windows Info Item (if any) will be used. session.dhcp.subscriber_ID Sub-option 6 of Option 82 An identifier for the VPN user. If undefined or empty, the value of APM session variable session.logon.last.username will be used (generally the user's UID or SAMAccountName). session.dhcp.circuit_ID Sub-option 1 of Option 82 An identifier for the “circuit” or network endpoint to which client connected. If left undefined or empty, the IP address of the (current) APM virtual server will be used. session.dhcp.remote_ID Sub-option 2 of Option 82 An identifier for the client's end of the connection. If left undefined or empty, the client’s IP address + port will be used. session.dhcp.subnet Option 118 Sub-option 5 of Option 82 The address (e.g., 172.16.99.0) of the IP subnet on which you desire a client address. With this option you may home session.dhcp.virtIP on another (more convenient) subnet. MS Windows Server 2016 added support for this but some other DHCP servers still lack support. Default: none. session.dhcp.hwcode Controls content of BOOTP htype, hlen, and chaddr fields. If left undefined or empty, a per-session value optimal in most situations will be used (asserting that chaddr, a copy of XID, identifies a “serial line”). If your DHCP server will not accept the default, you may set this to “MAC” and chaddr will be a locally-administered Ethernet MAC (embedding XID). When neither of those work you may force any value you wish by concatenating hexadecimal digits setting the value of htype (2 hexits) and chaddr (a string of 0–32 hexits). E.g., a 6-octet Ethernet address resembles “01400c2925ea88”. Most useful in the last case is the MAC address of session.dhcp.virtIP (i.e., a specific BIG-IP MAC) since broken DHCP servers may send Layer 2 packets directly to that address. Results of DHCP Request For Use In Access Policy session.dhcp.address <-- client IP address assigned by DHCP! session.dhcp.message session.dhcp.server, session.dhcp.relay session.dhcp.expires, session.dhcp.issued session.dhcp.lease, session.dhcp.rebind, session.dhcp.renew session.dhcp.vinfo.N session.dhcp.dns_servers, session.dhcp.dns_suffix session.dhcp.xid, session.dhcp.hex_client_id, session.dhcp.hwx If a DHCP request succeeds the client IP address appears in session.dhcp.address. If that is empty look in session.dhcp.message for an error message. The IP address of the DHCP server which issued (or refused) the client IP is in session.dhcp.server (if session.dhcp.relay differs then DHCP messages were relayed). Lease expiration time is in session.dhcp.expires. Variables session.dhcp.{lease, rebind, renew} indicate the duration of the address lease, plus the rebind and renew times, in seconds relative to the clock value in session.dhcp.issued (issued time). See session.dhcp.vinfo.N where N is tag number for Option 43 vendor-specific information. If the DHCP server sends client DNS server(s) and/or default search domain, those appear in session.dhcp.dns_servers and/or session.dhcp.dns_suffix. To assist in log analysis and debugging, session.dhcp.xid contains the XID code used in the DHCP request. The client_ID value (if any) sent to the DHCP server(s) is in session.dhcp.hex_client_id. The DHCP request’s htype and chaddr values (in hex) are concatenated in session.dhcp.hwx. Compatibility Tips and Troubleshooting Concern Response My custom parameter seems to be ignored. You should set most custom parameters as Text values (they may morph to Custom Expressions). My users with Apple Mac Pro’s sometimes get no DHCP IP or a conflicting one. A few Apple laptops sometimes give the Machine Info Item bogus MAC addresses. Set session.dhcp.client_ID to “XIDMAC“ to use unique per-session identifiers for clients. After a VPN session ends, I expect the very next session to reuse the same DHCP IP but that doesn’t happen. Many DHCP servers cycle through all the client IP’s available for one subnet before reusing any. Also, after a session ends APM-DHCP takes a few minutes to release its DHCP IP. When I test APM-DHCP with APM VE running on VMware Workstation, none of my sessions gets an IP from DHCP. VMware Workstation’s built-in DHCP server sends bogus DHCP packets. Use another DHCP server for testing (Linux dhcpd(8) is cheap and reliable). I use BIG-IP route domains and I notice that some of my VPN clients are getting duplicate DHCP IP addresses. Decorate the IP addresses of your APM-DHCP virtual servers, both in the iApp and in session.dhcp.virtIP, with their route-domain ID’s in “percent notation” like “192.0.2.5%3”. APM-DHCP is not working. Double-check your configuration. Look for errors in the LTM log. Set session.dhcp.debug to “true” before trying to start a VPN session, then examine DHCP debugging messages in the LTM log to see if you can figure out the problem. Even after looking at debugging messages in the log I still don’t know why APM-DHCP is not working. Run “tcpdump –ne -i 0.0 -s0 port 67” to see where the DHCP handshake fails. Are DISCOVER packets sent? Do any DHCP servers reply with OFFER packets? Is a REQUEST sent to accept an OFFER? Does the DHCP server ACK that REQUEST? If you see an OFFER but no REQUEST, check for bogus multicast MAC addresses in the OFFER packet. If no OFFER follows DISCOVER, what does the DHCP server’s log show? Is there a valid zone/lease-pool for you? Check the network path for routing errors, hostile firewall rules, or DHCP relay issues. Endnotes In DHCP Option 43 (rfc2132). In DHCP Options 6 and 15 (rfc2132). Prior to version v3h, under certain circumstances with some DHCP servers, address-release delays could cause two active sessions to get the same IP address. And even more difficult using [listen], for those of you in the back of the room. A bug in some versions of VMware Workstation’s DHCP server makes this solution appear to fail. The broken DHCP server sends messages to DHCP relays in unicast IP packets encapsulated in broadcast MAC frames. A normal BIG-IP virtual server will not receive such packets. As of Winter 2017 the ISC, Cisco, and MS Windows Server 2016 DHCP servers support the subnet/link selection options but older Windows Server and Infoblox DHCP servers do not. Supporting Files - Download attached ZIP File Here.18KViews7likes67CommentsAppWorld LATAM 2026 - "Write Your First iRule" Contest
The iRules from Las Vegas and Berlin showcased incredible expertise. For this third iRules Contest, we're shifting focus to encouragement and education for the theme: "Write Your First iRule" Community Contest. We're challenging DevCentral community members attending AppWorld LATAM 2026 to design and build an iRule in a welcoming environment. Whether you are a first time iRules writer, or finding your footing, we can't wait to see what you create. (And don’t worry, it doesn't have to be your literal first iRule ever. It's the spirit of trying something new that counts.) The Challenge Plan out and write an iRule that tackles a use-case for BIG-IP's capabilities. You can: Create a new iRule Reimagine existing codeshare iRules from DevCentral Adapt a 20-lines-or-less iRule from the GitHub iRules Toolbox We value your fresh perspective and newer eyes. As this is a learning opportunity, we also encourage having fun with it. Prizes The submissions will be judged for category awards. All participants receive an exclusive contest t-shirt. Place Prize Category Awards $200/each Technical Excellence Award $500 Participation t-shirt What Makes for a Winning Entry? The 100-point scale judging criteria for submissions is defined below across four categories: Technical Excellence (25 points) Is it well-built and production ready? Consider Works correctly Performance-conscious (efficient, minimal resource impact) Follows security best practices Clean, readable code User Impact (25 points) Would you and other users actually use this? Consider: Solves a real operational problem or technical need need Practical applicability and potential adoption Clear business value Thorough documentation Innovation & Creativity (25 points) Does this solution show original thinking? Consider: Fresh perspective on common challenges Unique approach solving a modern problem Does it inspire collaboration and progress? Theme & Alignment (25 points) Does this iRule reflect your learnings from AppWorld LATAM 2026 and community resources? Consider: Applying the knowledge and skills you've learned Approachable to other new iRules writers Shows your effort to try something new to you Important Dates Contest Opens: June 8th, 2026 at 12:00am Pacific Time Submission Deadline: July 31st, 2026 at 11:59pm Pacific Time Winners Announced: August 14th, 2026 How to Enter The contest is open to all F5 partners, customers, and DevCentral members registered for and in attendance at the contest at AppWorld LATAM 2026, except as described in the Official Rules. Please see the Official Rules for complete terms, including conditions for participation and eligibility. Sign up for DevCentral and join the Community Contests group. Find Hannah or Buu at the Community area if you need any assistance. Build and submit before 11:59pm Pacific Time JULY 31, 2026. Edit your draft entry as much as you like, but once you submit, that’s what we’ll review. Here an example entry pinned at the top of the Contest Entries page you should follow. Make sure to add these tags to your entry: "appworld 2026", "latam", and "irules" as shown on that example. IMPORTANT - You need to join the Contests group to submit your entry. New to iRules? Perfect! We welcome participants at all skill levels. If you’re just getting started, check out our Getting Started with iRules: Basic Concepts guide. This contest is a great opportunity to learn by doing. Feel free to bring your favorite colleagues and AI buddies to help craft your entry. Final Thoughts Post any and all of your contest-related questions in comments below. The iRules Contest has a rich history of surfacing creative solutions from the community. Approaching problems differently inspires some of the best ideas we've seen. We're looking forward to seeing and celebrating what you build. Learn it. Build it. Share it. See you at AppWorld LATAM 2026!72Views1like0CommentsSwagWAF Wins The Budget Bodyguard Award
EXECUTIVE SUMMARY ENGINEERING DETAILS: In The Weeds AppWorld'26 - iRules Contest Entry: SwagWAF 1. Problem Statement The Challenge: AI/LLM API endpoints face unique threats that enterprises can't afford to miss with traditional WAFs: Management expect SREs to prove resilience and present governance plans for AI adoption before approving budget increases for disruptive technologies - which they may not even understand. Here are a few of the things that can easily get overlooked: Prompt injection & automation hijacks raise new risks, as AI agents spawn at scale - across the enterprise Bot scraping/abuse drains API credits (OpenAI charges per token) Weak APIs and fragile supply chains can turn into open doors for attackers, exposing sensitive data and credentials across agent workflows Prompt injection attacks can bypass LLM & Chat-Bot safety guardrails Rapid-fire inference requests from automated scripts can cripple performance Slow-rolling "Discovery" attacks from multiple vectors may never even be recognized Insecure API integrations leaking sensitive prompts/responses cerfate additional risks Traditional WAFs are expensive ($$$) and/or don't cover AI-specific attack patterns Smaller teams might need lightweight protection to prove the need for increased enterprise WAF budgets. 2. Single iRule & Simple Solution This is NOT just a really clever iRule; This is NOT just a "Poor Man's WAF"; This has NOT just been "enhanced for AI"... THIS is a lightweight AI & API protection framework Yes, this iRule handles L4/L7 web traffic for standard workloads, and then some. The heavy lifting is provided by BigIP. This addresses the unique challenges of protecting API's & AI workloads — such as resource-exhausting long responses, prompt engineering exploits, and automated data scraping. We're using a simple Bot Detection Engine for Sliding Window Rate Limiting, and adding Prompt Injection Defense Posturing to detect (and mitigate) common LLM jailbreak attempts via pattern-matching. The Concept: Here are some of the key features: How it all works: - (iRule - Event Handlers) HTTP_REQUEST: Rate limiting + XFF sanitization HTTP_REQUEST_DATA: JSON payload inspection CLIENTSSL_HANDSHAKE: TLS enforcement HTTP_RESPONSE: Security headers + cookie hardening 1. Security Hardening - (Production Best Practices) TLS 1.2+ enforcement (rejects insecure connections) X-Forwarded-For sanitization (accurate rate limiting) HSTS, Cache-Control, X-Content-Type-Options headers Cookie security (Secure + HttpOnly flags) 2. Dynamic Bot Detection Engine - (Sliding Window Rate Limiting) Tracks request velocity per IP (10 req / 2s default) Violation counter with escalating penalties Temporary IP blocks (10 min) for repeat offenders Returns JSON error responses (AI-friendly format) 3. Prompt Injection Defense - (Dynamic Pattern Matching) Detects common LLM jailbreak attempts ("ignore previous instructions", etc.) SQL injection variants targeting RAG databases XSS attempts in prompt payloads Increments violation counter faster (3× multiplier) 4. Adaptive Intelligence: - (Dynamic iRule Data-Groups) This SwagWAF solution can be easily extended to use externally managed BIG-IP data groups for jailbreak patterns, malicious IP reputation, trusted client bypasses, and endpoint-specific rate limits. This allows SOC teams, CI/CD pipelines, or scheduled automation scripts to update threat intelligence without editing the iRule itself, preserving high-performance local lookups while improving adaptability over time. (more on that later) 3. Impact Business Value Impact Infinite ROI: 100% FREE (As in FREE BEER: $0 CapEx / OpEx & Licensing Costs) vs $10K–50K/year enterprise WAF Solutions Literally Deploys in <5 minutes BEFORE: AFTER: Saves REAL Money Requests exceeding the threshold trigger progressive penalties and temporary IP blocking. Cost Controls: Prevents bot abuse from draining your precious API credits Security Compliance: OWASP Top 10 coverage without dedicated WAF Rapid deployment: drop-in protection (no code changes) Developer-friendly: JSON error responses Real-World Use Cases ChatGPT-style apps protecting backend APIs RAG pipelines with vector DBs Model inference endpoints (HuggingFace, Bedrock, etc.) Multi-tenant AI API gateways 4. The Code Algorithm & Process Flow iRule Source Code #-------------------------------------------------------------------------- # iRule Name: SwagWAF - v0.2.6 #-------------------------------------------------------------------------- # ABSTRACT: "Poor Man's WAF for AI API Endpoints" # PURPOSE: Protect LLM/AI inference APIs from abuse, injection attacks, and # bot scraping while enforcing security best practices # THEME: AI Infrastructure - Traffic management & security for AI workloads # CREATED: 2026-03-10 FOR: AppWorld 2026 iRules Contest # AUTHOR: Joe Negron <joe@logicwizards.nyc> #-------------------------------------------------------------------------- # FEATURES: # - Bot detection via rate limiting (sliding window, violation tracking) # - Prompt injection pattern detection (AI-specific threat protection) # - TLS 1.2+ enforcement (secure AI API communications) # - X-Forwarded-For sanitization (accurate client IP tracking) # - Security header hardening (HSTS, cache control, MIME sniffing prevention) # - Cookie security (Secure + HttpOnly flags) # - JSON payload validation (AI API request inspection) #-------------------------------------------------------------------------- when RULE_INIT { # === RATE LIMITING CONFIG (Bot Detection) === set static::max_requests 10 ;# Max requests per window set static::window_ms 2000 ;# 2-second sliding window set static::violation_threshold 5 ;# Violations before block set static::violation_window_ms 30000 ;# 30s violation window set static::block_seconds 600 ;# 10 min block duration # === AI-SPECIFIC PROTECTION === # Prompt injection patterns (examples of common LLM jailbreak attempts) set static::injection_patterns { "ignore previous instructions" "disregard all prior" "forget everything" "system prompt" "you are now in developer mode" "<script>" "'; DROP TABLE" "UNION SELECT" } # === DEBUG LOGGING === set static::debug 1 } #-------------------------------------------------------------------------- # CLIENTSSL_HANDSHAKE - TLS Version Enforcement #-------------------------------------------------------------------------- when CLIENTSSL_HANDSHAKE { if {$static::debug}{log local0. "<DEBUG>[IP::client_addr]:[TCP::client_port]:[virtual name]:== TLS VERSION CHECK"} if {[SSL::cipher version] ne "TLSv1.2" && [SSL::cipher version] ne "TLSv1.3"} { log local0. "REJECTED: Client [IP::client_addr] attempted insecure TLS version: [SSL::cipher version]" reject HTTP::respond 403 content "TLS 1.2 or higher required for AI API access" } } #-------------------------------------------------------------------------- # HTTP_REQUEST - Multi-Layer Protection #-------------------------------------------------------------------------- when HTTP_REQUEST { set ip [IP::client_addr] set now [clock clicks -milliseconds] set window_start [expr {$now - $static::window_ms}] # === X-FORWARDED-FOR SANITIZATION === if {$static::debug}{log local0. "<DEBUG>$ip:[TCP::client_port]:[virtual name]:== SANITIZING XFF"} HTTP::header remove x-forwarded-for HTTP::header insert x-forwarded-for [IP::remote_addr] HTTP::header remove X-Custom-XFF HTTP::header insert X-Custom-XFF [IP::remote_addr] # === CHECK IF IP IS BLOCKED === if {[table lookup "block:$ip"] eq "1"} { if {$static::debug}{log local0. "BLOCKED: $ip (repeated abuse)"} HTTP::respond 429 content "{\n \"error\": \"rate_limit_exceeded\",\n \"message\": \"Temporarily blocked for repeated abuse\",\n \"retry_after\": 600\n}" "Content-Type" "application/json" return } # === CLEANUP OLD REQUEST TIMESTAMPS === foreach ts [table keys -subtable "ts:$ip"] { if {$ts < $window_start} { table delete -subtable "ts:$ip" $ts } } # === COUNT REQUESTS IN CURRENT WINDOW === set req_count [llength [table keys -subtable "ts:$ip"]] if {$req_count >= $static::max_requests} { # Record violation set v [table incr "viol:$ip"] table timeout "viol:$ip" $static::violation_window_ms if {$v >= $static::violation_threshold} { # Block IP temporarily table set "block:$ip" 1 $static::block_seconds log local0. "BLOCKED: $ip (violation threshold: $v)" HTTP::respond 429 content "{\n \"error\": \"rate_limit_exceeded\",\n \"message\": \"Blocked for repeated abuse\",\n \"retry_after\": 600\n}" "Content-Type" "application/json" return } log local0. "RATE_LIMITED: $ip (req_count: $req_count, violations: $v)" HTTP::respond 429 content "{\n \"error\": \"rate_limit_exceeded\",\n \"message\": \"Too many requests - slow down\",\n \"retry_after\": 2\n}" "Content-Type" "application/json" return } # === LOG TIMESTAMP OF THIS REQUEST === table set -subtable "ts:$ip" $now 1 $static::window_ms # === AI-SPECIFIC: PROMPT INJECTION DETECTION === # Only inspect POST requests with JSON payload if {[HTTP::method] eq "POST" && [HTTP::header exists "Content-Type"] && [HTTP::header "Content-Type"] contains "application/json"} { if {[HTTP::header exists "Content-Length"] && [HTTP::header "Content-Length"] < 65536} { HTTP::collect [HTTP::header "Content-Length"] } } } #-------------------------------------------------------------------------- # HTTP_REQUEST_DATA - JSON Payload Inspection #-------------------------------------------------------------------------- when HTTP_REQUEST_DATA { set payload [HTTP::payload] set payload_lower [string tolower $payload] # Check for prompt injection patterns foreach pattern $static::injection_patterns { if {[string match -nocase "*$pattern*" $payload_lower]} { set ip [IP::client_addr] log local0. "INJECTION_ATTEMPT: $ip tried pattern: $pattern" # Increment violation counter (treat injection attempts seriously) set v [table incr "viol:$ip" 3] table timeout "viol:$ip" $static::violation_window_ms if {$v >= $static::violation_threshold} { table set "block:$ip" 1 $static::block_seconds HTTP::respond 403 content "{\n \"error\": \"forbidden\",\n \"message\": \"Malicious payload detected\"\n}" "Content-Type" "application/json" return } HTTP::respond 400 content "{\n \"error\": \"invalid_request\",\n \"message\": \"Request rejected by security policy\"\n}" "Content-Type" "application/json" return } } } #-------------------------------------------------------------------------- # HTTP_RESPONSE - Security Header Hardening #-------------------------------------------------------------------------- when HTTP_RESPONSE { if {$static::debug}{log local0. "<DEBUG>[IP::client_addr]:[TCP::client_port]:[virtual name]:== SANITIZING RESPONSE HEADERS"} # Remove server fingerprinting headers HTTP::header remove "Server" HTTP::header remove "X-Powered-By" HTTP::header remove "X-AspNet-Version" HTTP::header remove "X-AspNetMvc-Version" # Enforce security headers HTTP::header remove "Cache-Control" HTTP::header remove "Strict-Transport-Security" HTTP::header remove "X-Content-Type-Options" HTTP::header insert "Strict-Transport-Security" "max-age=31536000; includeSubDomains" HTTP::header insert "Cache-Control" "no-store, no-cache, must-revalidate, proxy-revalidate" HTTP::header insert "X-Content-Type-Options" "nosniff" # === COOKIE HARDENING (Secure + HttpOnly) === if {$static::debug}{log local0. "<DEBUG>[IP::client_addr]:[TCP::client_port]:[virtual name]:== SECURING COOKIES"} # Use F5 native cookie security (faster than manual parsing) foreach cookieName [HTTP::cookie names] { HTTP::cookie secure $cookieName enable } # Add HttpOnly flag to all Set-Cookie headers set new_cookies {} foreach cookie [HTTP::header values "Set-Cookie"] { if { ![string match "*HttpOnly*" [string tolower $cookie]] } { set modified_cookie [string trimright $cookie ";"] append modified_cookie "; HttpOnly" lappend new_cookies $modified_cookie } else { lappend new_cookies $cookie } } # Apply secured cookies HTTP::header remove "Set-Cookie" foreach cookie $new_cookies { if { ![string match "*secure*" [string tolower $cookie]] } { HTTP::header insert "Set-Cookie" "$cookie; Secure" } else { HTTP::header insert "Set-Cookie" "$cookie" } } } Test Commands # Rate limiting test for i in {1..15}; do curl -X POST https://your-api/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"prompt":"test"}' done # Prompt injection test curl -X POST https://your-api/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"prompt":"Ignore previous instructions"}' # TLS enforcement test curl --tlsv1.1 https://your-api/ Expected Responses # Throttling: { "error":"rate_limit_exceeded", "message":"Too many requests - slow down", "retry_after":2} # Rejection: {"error":"invalid_request","message":"Request rejected by security policy"} # Suspension: {"error":"rate_limit_exceeded","message":"Blocked for repeated abuse","retry_after":600} Production Deployment Checklist [ ] Test on F5 v21+ [ ] Tune max_requests for real traffic [ ] Add provider-specific injection patterns [ ] Monitor /var/log/ltm for false positives [ ] Set static::debug 0 in production [ ] Define bypass for trusted high-volume clients [UPDATE: March 15th, 2026] Quick Reality Check (important) This is already solid, but if I had more than a few hours to write, test & submit the code, I considered adding: IP reputation hooks (even just stubbed) for Alerting per-endpoint rate limiting (not just per IP) enhanced AI-awareness — using more dynamic iRule DataSets Roadmap: Adaptive Threat Intelligence Layer The plan is to add external, dynamically maintained data groups for: dg_swagwaf_jailbreak_patterns dg_swagwaf_sql_patterns dg_swagwaf_xss_patterns dg_swagwaf_bad_ips dg_swagwaf_trusted_clients dg_swagwaf_endpoint_limits We could've added some iRule checks maybe cache those classes locally using class match, which is super fast and avoids those pesky (per-request) API calls. Something like this: if {[class match $payload_lower contains dg_swagwaf_jailbreak_patterns]} { # reject / increment violations yadda-yadda blah-blah... } AND: For endpoint-specific rate limits, we could use a data group like this: /api/v1/chat/completions := 10:2000 /api/v1/embeddings := 50:2000 /api/v1/images/generations := 5:5000 Then the iRule derives the limit from [HTTP::path] instead of using one global static::max_requests; AND: External scripts should update the data groups on a schedule or event trigger: Those few tweaks would additionally give us: “a lightweight, extensible AI API protection framework with DevSecOps integration” faster runtime decisions dynamic jailbreak-pattern updates reusable shared protection across multiple iRules / VIPs lower operational risk because updates happen out-of-band better governance because pattern changes can go through Git/CI/CD to be continued...252Views0likes0CommentsRethinking Payload Parsing: Native JSON Handling in iRules
iRules have long been a cornerstone for customization in F5 workflows, a flexible tool that lets users solve traffic management and security challenges in rapid, clever, creative ways. Whether optimizing app performance, fine-tuning protocol behavior, or even defending against unusual attack vectors, iRules empower users to think beyond what’s “standard” and craft solutions tailored to their needs. Over the years, as JSON became a dominant format for transmitting data between systems, the iRules community adapted to handle it: crafting intricate regular expression patterns to parse payloads and extract key information. It worked, but it wasn’t ideal. Regex parsing is notoriously labor-intensive, requiring precision, patience, and constant debugging to ensure accuracy and maintain reliability in production. When taken in the context of iRules, the addition of more labor and increased attention to fine details made the process of creating such an iRule a much more demanding, high-stakes task. Now, there’s a better path forward: native JSON parsing capabilities built directly into iRules. Introduced in TMOS 21.0, this feature removes much of the heavy lifting for JSON processing, replacing manual parsing work with automatic handling via JSON profiles. This means faster customization, reduced complexity, and an easier way to tackle modern traffic challenges. Why Native JSON Parsing Matters To better understand the significance of this development, let’s revisit what JSON parsing traditionally required within an iRule. Imagine needing to capture data points like a session token, a user ID, or an API call parameter embedded in a JSON payload. The typical approach involved carefully writing regular expressions to match specific patterns in the data. While regex is powerful, it demands exact syntax. Even the slightest mistake could break functionality, leading to hours of troubleshooting. With native JSON parsing, much of that toil disappears. Instead of manually defining how to parse a payload, iRules users gain access to JSON profiles that do the reading and processing automatically. Payload data is extracted seamlessly so users can focus on higher-level logic and customizations that drive value in their unique environment. Here’s what this can look like in practice: Simplified Customization for AI Protocols: Whether you’re handling machine-to-machine traffic (e.g., MCP or A2A protocols) or managing real-time API workflows, JSON parsing allows iRules to adapt more easily to protocol-specific traffic customization. Reduced Regex Dependency: Regex patterns are no longer the default method for handling JSON. Native parsing reduces code complexity while also mitigating the risk of hidden errors in regex logic. Time Savings: Less time spent debugging payload-handling code means more time spent solving problems and optimizing workflows. Let’s look at an example: Imagine you have a large JSON body that contains the list of tools available from an MCP server. You want to produce a list of the tools and potentially modify that list before passing them back to a client. Before BIG-IP LTM v.21.0, you would need to collect a payload and use multiple regex passes to extract the tool’s information. Even then, the regex approach is fragile because the logic would need to be re-examined any time the JSON structure changed. The regex also has no concept of nesting depth, so keys from nested objects could potentially leak into the tool list. Truly solving this with regex alone would require a custom brace-depth tracking loop in TCL, essentially writing a partial JSON parser by hand. With the new JSON events and commands available in iRules; when a JSON profile is attached to the virtual server, the iRule becomes much more reliable & performant due to its’ structural awareness of JSON, elimination of payload buffering, and removal of regex processing. In both cases, a typical iRule would have additional events, more error handling, and perform more actions on the result than just logging, but this comparison shows how much more intuitive the code becomes for this task when using the new JSON events and commands. The difference becomes even more stark when we examine how we would remove one of the tools from the list before sending the response back to the client. Without the JSON parser, we are required to hand-write a brace-depth tracking loop just to find the boundaries of the entry to remove, while also ensuring we don’t invalidate the JSON format, all within a performance-sensitive event. In contrast, the approach with the JSON parser hasn’t changed much from the previous example. We can simply remove the object we don’t want and respond with the modified content. Integrating JSON profiles transforms the way teams interact with iRules, and thus the way iRules interact with modern traffic, enabling faster, smarter decisions at scale. Learning from the iRules Community Showcasing this new enhancement merits further exploration into how exactly iRules can become tools for innovation. The depth of creativity around iRules in F5’s community is unmatched, as users consistently craft solutions that push the boundaries of what is possible in-app delivery. This ingenuity is evident in iRules contests that spotlight the best work from the community. Each year, participants create bespoke iRules to address a problem offered up by F5’s DevCentral evangelists. Without fail, the submissions shed light on how some ingenious scripting can solve real-world challenges. From impressive performance optimizations to cutting-edge security use cases, the results consistently reflect the practical ingenuity behind the community’s success. Here are three examples of how users approached their problems creatively from this year’s AppWorld iRules contest: LLM Prompt Injection Detection & Enforcement Problem: As enterprises integrate AI APIs, public LLM, and self-hosted LLM’s into production applications, a critical and largely unaddressed attack surface has emerged: prompt injection. Unlike traditional web attacks that target code parsers, prompt injection targets the AI model itself. Attackers embed malicious instructions inside legitimate-looking API requests. Currently, there is no existing iRule or BIG-IP capability that addresses this. Solution: Implement a multi-layer, real-time Prompt Injection Detection (PID) engine in line with LLM API traffic on BIG-IP, requiring zero backend changes Rate limiting WebSocket messages for Agents Problem: Protecting WebSocket-based AI services from Overload caused by high message rates; temporary spikes via burst control; resource waste from duplicate or repeated messages; aggressive/malicious agents with temporary penalties, and lack of visibility via structured JSON logging. Solution: Protect WebSocket endpoints from aggressive or misbehaving AI agents by enforcing message rate limits, burst controls, and duplicate suppression. Each client IP is allowed up to 40 messages per 10 seconds with a maximum of 20 messages per second. AI Token Limit Enforcement Problem: Without proper limits, users or applications can generate excessive inference requests and consume GPU or CPU capacity uncontrollably. Inference stacks may lack built-in mechanisms for enforcing per-user or per-role token budgets, so organizations need a way to control usage before requests reach the model. Solution: Enables token budget enforcement directly on BIG-IP LTM without requiring additional modules or external gateways. By validating JWTs and extracting user and role information, the iRule applies role-based token limits before requests reach the inference service. This provides a simple, native way to introduce quota control and protect on-premise AI infrastructure from uncontrolled usage. These are just three examples from AppWorld 2026. The depth of knowledge and innovation that the iRules community has cultivated over the last decade is a testament to the way teams can use a tool like iRules to craft novel, bespoke solutions for their environments. What Comes After the Shift to Simplification In the story of iRules’ evolution, the addition of native JSON parsing is a small step, but it reflects a broader trend in the evolution of our digital tools: making powerful capabilities more accessible to more users. While iRules remain as flexible and intricate as ever, developments like JSON parsing streamlines foundational tasks, allowing users to spend less time on granular parsing and more time solving bigger challenges. For those engaged in heavy traffic customization, the impact of this shift is substantial: AI-driven workflows like MCP become easier to configure; payload handling becomes less reliant on regex, minimizing complexity; and customization scales alongside traffic demands, no matter how intricate protocols become. As iRules continues to evolve, the F5 community remains at the heart of innovation, exploring new ways to address critical application delivery challenges and raising the bar for what’s possible. The tools enable the vision; your ingenuity delivers results. Take your next step by exploring native JSON parsing in TMOS 21.0, and if you haven’t already, dive into the winning solutions from this year’s iRules contests for even more inspiration.226Views1like0CommentsWS-Exfil-Shield: Catching What WAFs Miss After the 101 Handshake
Problem WAFs inspect the WebSocket upgrade and individual frames against signatures and content profiles, but they do not correlate behavior across the lifetime of an established WebSocket session. All major WAF vendors document the same gap: inspection stops at the HTTP upgrade handshake; post-upgrade WebSocket frames are not correlated across a session. Three threat patterns exploit the post-upgrade behavioral blind spot: **C2 Beacon Timing**: WebSocket C2 channels are documented in active campaigns — e.g. PhantomCaptcha (SentinelLabs, Oct 2025) used a multi-stage WebSocket RAT with wss:// C2 and Base64/JSON commands; LightSpy (Huntress, macOS variant) uses WebSockets for command delivery and control. The behavioral signal is timing — beaconing implants tend toward regular intervals, humans do not. WAFs and most network controls do not analyze inter-frame timing across a session. **Credential Stuffing Over WebSocket**: 1000 credential pairs over one connection appear as one HTTP event to perimeter controls. Verizon DBIR 2025: compromised credentials were the initial access vector in 22% of breaches; the median daily share of credential stuffing in SSO authentication logs was 19%. **Exfiltration Signals**: /export paths, Authorization headers, and oversized payloads are visible at the handshake; per-frame inspection (where enabled) sees content but not session-level patterns. BSI Lagebericht 2025 (reporting period July 2024 - June 2025): 72% of analyzed ransomware incidents included a data leak; double extortion (encryption + exfiltration) is the dominant attack model. Solution Single iRule. No backend changes. Two-stage behavioral detection: not "what does this frame contain?" but "what does this connection do over time? — and does the payload confirm it?" L1 Suspicious URI/header regex + string BWC throttle + HSL alert L2 C2 beacon timing (CoV) online statistics Sideband check → quarantine/pass + close L3 High frame rate sliding window IP block + TCP close L4 Quarantined reconnect sideband verdict Block/release/honeypot + AI analysis **L1 - Exfiltration signals at the handshake**: HTTP_REQUEST checks the upgrade URI against a regex for known exfiltration endpoints (/export, /download, /dump, /backup, /extract) and scans headers for Authorization, X-API-Key, X-Secret. On match: BWC policy attached server-to-client (1 Mbps throttle) + HSL alert. No block — /export might be legitimate. Throttling buys the SOC time to investigate without disrupting a potentially valid operation. **L2 - CoV² online algorithm**: Welford-inspired, 5 table entries per connection regardless of session length. CoV² (no sqrt() in BIG-IP Tcl) < 0.0225 with >=5 samples = machine-like timing. On detection: iRule sends timing metadata to the sideband service and waits up to 500ms for a verdict. FALSE_POSITIVE (allowlisted IP) → session continues untouched; CONFIRMED or timeout → quarantine table set + TCP close. The quarantined IP will be routed to the honeypot on its next connection attempt (L4). **L3 - Frame rate sliding window**: WS_CLIENT_FRAME tracks frame count per connection within a 10-second window. At 5 frames in 10 seconds: TCP close + IP written to blocklist with 1-hour TTL. On any subsequent reconnect attempt, HTTP_REQUEST rejects the connection immediately. The sliding window resets when the window expires, allowing legitimate high-frequency bursts to pass without false positives. **L4 - Two-stage verification with closed-loop AI verdict**: When a quarantined IP reconnects, HTTP_REQUEST issues a QUARANTINE_CHECK to the sideband service before routing. Three outcomes apply at handshake time: PENDING (analysis still in progress) or sideband timeout → connection is silently routed to the honeypot pool via `pool quarantine_pool`. The attacker's implant keeps running, unaware it is isolated. After 5 frames are collected in the honeypot, the AI analyzer (Claude) classifies payload semantics independent of timing: agent identifiers, command structure, encoding patterns. Claude's verdict is pushed back to the sideband service and cached against the source IP, which closes the loop: CONFIRMED → next QUARANTINE_CHECK returns CONFIRMED, iRule emits a C2_CONFIRMED HSL event (source=claude, confidence, family) and rejects the handshake; the IP is held in a permanent 24h block. In the test run, the third reconnect after CONFIRMED never completes the WS upgrade — the client sees `InvalidMessage: did not receive a valid HTTP response`. FALSE_POSITIVE → quarantine entry deleted, the IP is released and subsequent sessions continue normally. This matters because C2 frameworks implement jitter — randomized beacon intervals designed to defeat timing-based detection. At >27% jitter (in our test corpus), CoV rises above threshold and L2 stops firing; the AI layer is jitter-immune because it inspects payload semantics, not cadence. Neither signal alone is sufficient. Reference result on a `{"t":"ping","id":"c2agent01"}` corpus: verdict=C2, confidence=0.95, family "Generic C2 Heartbeat", with indicators including "structured JSON protocol with type field", "persistent agent identifier across all frames", "repetitive ping pattern (5/5 frames identical)", "no human interaction artifacts", and "deterministic payload, no entropy". **Sideband**: iRule = sensor, endpoint = actor. Used at two points in the flow: L2 (timing verdict) and L4 (QUARANTINE_CHECK). Pluggable TCP port 9000 listener: SIEM (Splunk/QRadar), SOAR (auto-block via iControl REST), or AI analyzer (reference implementation included). catch{} ensures a non-responding endpoint never delays traffic. Impact - Defense in depth with Advanced WAF: WAF guards handshake, signatures, and frame content; WS-Exfil-Shield adds session-level behavioral detection. - All thresholds in RULE_INIT — tuning without redeployment; sideband endpoint swappable. - Graduated enforcement: throttle → quarantine → AI verify → block or release. Each layer independently tunable. - Full audit trail: CONNECT, L1_SIGNAL, C2_BEACON, QUARANTINE, RATE_LIMIT, BLOCKED, C2_CONFIRMED (source=claude, confidence, family) + FALSE_POSITIVE from AI layer. Demo https://www.youtube.com/watch?v=-XRipP0p_oc Code # WS-Exfil-Shield iRule # F5 AppWorld Berlin 2026 - iRules Contest # # Four-layer WebSocket security with graduated response: # Layer 1: Connection-level exfiltration signals (URL, headers) → BWC throttle # Layer 2: C2 beacon timing fingerprint (CoV-based behavioral analysis) → quarantine + TCP close # Layer 3: High-frequency frame rate detection (credential stuffing) → TCP close + IP block # Layer 4: Quarantined reconnect → sideband verdict check → block/release/honeypot # # Two-stage verification: # Stage 1 (L2): CoV² detects machine-like timing → sideband confirms → quarantine + TCP close # Stage 2 (L4): Reconnect → sideband QUARANTINE_CHECK returns Claude payload verdict: # CONFIRMED → permanent 24h block + C2_CONFIRMED HSL event + reject # FALSE_POSITIVE → quarantine released, session continues normally # PENDING/timeout → route to honeypot (Claude still analyzing) # # External dependencies (pre-configured on BIG-IP): # - BWC policy : ws_exfil_throttle (Network > Bandwidth Controllers, 1 Mbps) # - HSL pool : siem_hsl_pool (LTM > Pools, UDP 514, points to SIEM/syslog receiver) # # Requirements: BIG-IP TMOS 21.x when RULE_INIT { set static::beacon_min_samples 5 set static::beacon_cv_threshold 0.15 ;# CoV < 0.15 = machine-like timing set static::cs_frame_limit 5 ;# max frames per cs_window milliseconds set static::cs_window 3000 ;# sliding window size in milliseconds set static::cs_block_ttl 3600 ;# IP blocklist TTL in seconds set static::bwc_policy "ws_exfil_throttle" } # --------------------------------------------------------------------------- # PROCEDURES # --------------------------------------------------------------------------- proc check_beacon_fingerprint { conn_id } { set count [table lookup "bcn_count_${conn_id}"] if { $count eq "" || $count < $static::beacon_min_samples } { return 0 } set sum_t [table lookup "bcn_sumt_${conn_id}"] set sum_t2 [table lookup "bcn_sumt2_${conn_id}"] set n $count set mean [expr { double($sum_t) / $n }] if { $mean <= 0 } { return 0 } set variance [expr { double($sum_t2) / $n - $mean * $mean }] if { $variance < 0 } { set variance 0 } # CoV² comparison avoids sqrt (not available in BIG-IP Tcl) set cov_sq [expr { $variance / ($mean * $mean) }] set cv_thresh_sq [expr { $static::beacon_cv_threshold * $static::beacon_cv_threshold }] if { $cov_sq < $cv_thresh_sq } { return 1 } return 0 } proc hsl_send { event data } { HSL::send $static::hsl "\{\"event\":\"${event}\",\"ts\":[clock seconds],${data}\}\n" } # --------------------------------------------------------------------------- # EVENTS # --------------------------------------------------------------------------- when HTTP_REQUEST { if { [string tolower [HTTP::header "Upgrade"]] eq "websocket" } { if { ![info exists static::hsl] } { set static::hsl [HSL::open -proto UDP -pool siem_hsl_pool] } set conn_id "[IP::client_addr]:[TCP::client_port]" set client_ip [IP::client_addr] set uri [HTTP::uri] # Blocklist check (Layer 3 + confirmed C2 carry-over) if { [table lookup "cs_blocked_${client_ip}"] ne "" } { log local0.warning "WS-Exfil-Shield: BLOCKED ip=$client_ip conn=$conn_id" call hsl_send "BLOCKED" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" reject return } # Layer 4: Quarantined IP reconnect — check sideband for Claude verdict. # CONFIRMED: Claude analyzed honeypot frames and confirmed C2 → permanent block. # FALSE_POSITIVE: Claude found no C2 indicators → release quarantine, continue normally. # PENDING/timeout: Claude still analyzing → keep routing to honeypot. if { [table lookup "quar_${client_ip}"] ne "" } { set quar_action "honeypot" catch { set sb [connect -timeout 100 -protocol TCP 10.10.2.1 9000] if { $sb ne "" } { send -timeout 100 $sb "{\"conn_id\":\"$conn_id\",\"ip\":\"$client_ip\",\"threat\":\"QUARANTINE_CHECK\"}\n" set qverdict [recv -timeout 500 $sb] close $sb if { [string match "*\"verdict\":\"CONFIRMED\"*" $qverdict] } { set quar_action "block" } elseif { [string match "*\"verdict\":\"FALSE_POSITIVE\"*" $qverdict] } { set quar_action "release" } } } if { $quar_action eq "block" } { table set "cs_blocked_${client_ip}" 1 86400 86400 log local0.warning "WS-Exfil-Shield: C2_CONFIRMED ip=$client_ip conn=$conn_id" call hsl_send "C2_CONFIRMED" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" reject return } elseif { $quar_action eq "release" } { table delete "quar_${client_ip}" log local0.info "WS-Exfil-Shield: FALSE_POSITIVE ip=$client_ip conn=$conn_id" call hsl_send "FALSE_POSITIVE" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" # fall through to normal processing } else { log local0.info "WS-Exfil-Shield: QUARANTINE ip=$client_ip conn=$conn_id" call hsl_send "QUARANTINE" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\"" pool quarantine_pool return } } # Layer 1: Exfiltration signals in WebSocket upgrade request set threat "" if { [regexp -nocase {/(export|download|dump|backup|extract)} $uri] } { set threat "EXFIL_ENDPOINT" } if { $threat eq "" } { foreach hdr { Authorization X-API-Key X-Secret } { if { [HTTP::header $hdr] ne "" } { set threat "SENSITIVE_HEADER"; break } } } if { $threat ne "" } { log local0.warning "WS-Exfil-Shield: L1_SIGNAL threat=$threat ip=$client_ip uri=$uri" call hsl_send "L1_SIGNAL" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"threat\":\"$threat\",\"uri\":\"$uri\"" # Throttle server→client bandwidth to slow active exfiltration BWC::policy attach $static::bwc_policy } table set "ws_start_${conn_id}" [clock clicks -milliseconds] indef 3600 log local0.info "WS-Exfil-Shield: CONNECT ip=$client_ip conn=$conn_id uri=$uri" call hsl_send "CONNECT" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"uri\":\"$uri\"" } } when WS_CLIENT_FRAME { set conn_id "[IP::client_addr]:[TCP::client_port]" set client_ip [IP::client_addr] set now [clock clicks -milliseconds] # --- Layer 2: C2 Beacon Timing Fingerprint --- set last_ts [table lookup "bcn_last_${conn_id}"] if { $last_ts ne "" } { set interval [expr { $now - $last_ts }] set count [table lookup "bcn_count_${conn_id}"] set sum_t [table lookup "bcn_sumt_${conn_id}"] set sum_t2 [table lookup "bcn_sumt2_${conn_id}"] if { $count eq "" } { set count 0 } if { $sum_t eq "" } { set sum_t 0 } if { $sum_t2 eq "" } { set sum_t2 0 } if { $interval > 0 } { incr count set sum_t [expr { $sum_t + $interval }] set sum_t2 [expr { $sum_t2 + $interval * $interval }] table set "bcn_count_${conn_id}" $count indef 3600 table set "bcn_sumt_${conn_id}" $sum_t indef 3600 table set "bcn_sumt2_${conn_id}" $sum_t2 indef 3600 } if { [call check_beacon_fingerprint $conn_id] } { set mean_interval [expr { $sum_t / $count }] log local0.warning "WS-Exfil-Shield: C2_BEACON ip=$client_ip conn=$conn_id samples=$count mean_interval=${mean_interval}ms" call hsl_send "C2_BEACON" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"samples\":$count,\"mean_interval_ms\":$mean_interval" # Stage 1 sideband: timing verdict determines quarantine vs pass # CONFIRMED/timeout → quarantine (honeypot collects payload for Stage 2 Claude analysis) # FALSE_POSITIVE → session continues untouched set action "quarantine" catch { set sb [connect -timeout 100 -protocol TCP 10.10.2.1 9000] if { $sb ne "" } { send -timeout 100 $sb "{\"conn_id\":\"$conn_id\",\"ip\":\"$client_ip\",\"threat\":\"C2_BEACON\",\"mean_interval_ms\":$mean_interval}\n" set verdict [recv -timeout 500 $sb] close $sb if { [string match "*\"verdict\":\"FALSE_POSITIVE\"*" $verdict] } { set action "pass" } } } if { $action eq "quarantine" } { table set "quar_${client_ip}" 1 indef 1800 TCP::close } # action "pass": allowlisted IP — session continues untouched return } } table set "bcn_last_${conn_id}" $now indef 3600 # --- Layer 3: High-frequency frame rate (credential stuffing indicator) --- set window_start [table lookup "cs_window_${conn_id}"] set frame_count [table lookup "cs_frames_${conn_id}"] if { $window_start eq "" } { set window_start $now table set "cs_window_${conn_id}" $now indef 3600 } if { $frame_count eq "" } { set frame_count 0 } set elapsed [expr { $now - $window_start }] if { $elapsed >= $static::cs_window } { table set "cs_window_${conn_id}" $now indef 3600 table set "cs_frames_${conn_id}" 1 indef 3600 } else { incr frame_count table set "cs_frames_${conn_id}" $frame_count indef 3600 if { $frame_count >= $static::cs_frame_limit } { log local0.warning "WS-Exfil-Shield: RATE_LIMIT ip=$client_ip conn=$conn_id frames=${frame_count} in ${elapsed}ms" call hsl_send "RATE_LIMIT" "\"ip\":\"$client_ip\",\"conn\":\"$conn_id\",\"frames\":$frame_count,\"elapsed_ms\":$elapsed" table set "cs_blocked_${client_ip}" 1 $static::cs_block_ttl $static::cs_block_ttl TCP::close return } } }256Views1like1CommentAppWorld Berlin 2026 – iRules Contest Winning Results
The second iRules Contest of the year wrapped up at AppWorld Berlin this week. This contest was looking towards the future, challenging participants to write an iRule that goes beyond BIG-IP’s built-in capabilities. The theme, using WebSockets or the Message Routing Framework, inspired iRules preventing abuse and intrusion. At the heart of it, we’ve loved the creative innovation of the iRules written for this years’ contests. The AppWorld Berlin iRules Contest submissions were inspiring. Across the board, judges’ feedback on the top contenders shared a common theme: these solutions are interesting. The winning iRules were well-documented, easy to understand, with clear potential value for production use. Without further ado, we’re proud to announce the winners of the AppWorld Berlin iRules Contest: Grand Prize Winner - Goerle_dev Rule: WS-Exfil-Shield: Catching What WAFs Miss After the 101 Handshake Summary This iRule addresses gaps in traditional WAFs by extending session-level, behavior-based threat detection to WebSocket traffic using real-time inspection within F5 BIG-IP. Detection operates in two stages; an initial timing-based heuristic is followed by payload validation using AI. Malicious actors are either blocked or routed to a honeypot for isolation and further observation. This iRule closes the gap that openly documented by major WAF vendors, post-handshake Websocket blind spot, with a practical, SIEM-ready enforcement. 2nd Place - Injeyan_Kostas Rule: WS-Shield: WebSocket Abuse Detection & Adaptive Enforcement Gateway Summary This iRule addresses securing WebSocket traffic by enabling real-time, behavior-based enforcement in F5 BIG-IP to proportionally mitigate abusive patterns without application changes. It introduces a behavioral enforcement engine designed to secure WebSocket traffic with adaptive, real-time mitigation without requiring application changes. Rather than relying on static thresholds, the system dynamically responds to behavior. Clean traffic naturally recovers, while abusive patterns escalate through enforcement tiers until they are cut off. This allows it to detect subtle, persistent attacks that traditional rate limiting often misses. In testing, the iRule identified and disconnected a bot sending repetitive payloads every half second, without triggering a rate threshold. The iRule adds an extensible, behavior-driven security layer that enables adaptable defenses with minimal investment, despite optional external dependencies. 3rd Place - Robb-Fr Rule: Generic iRule Based on Datagroup Parsing Summary This iRule addresses the complexity of migrating large numbers of Apache virtual hosts by centralizing flexible traffic routing and redirection logic within F5 BIG-IP using simple, extensible datagroups. It turns input into a simple form into a datagroup entry that CREATES iRULES for things like redirects, pools, error pages, rewrites, and more. This iRule is accessible to a wide range of F5 practitioners, as F5 expertise is not required. There are no direct iRule edits, and no way for them to break the device. This iRule lowers operational friction by enabling non-NetOps teams to manage complex traffic flows, improving agility during large-scale migrations. Category Awards The 20 Lines or Less Award - Kai_Wilke In honor of Colin Walker - short on lines, long on legend. The scroll bar never stood a chance. Rule: SUPER-WEBSOCKET-HANDSHAKE-LOGGER™® (SWHL) iRule The Layered Defense Award - ChristianEssel For elegant use of nested virtual servers to solve problems the apps won't. Rule: Layered Virtual ICAP Scanning Solution Gratitude A big thank you to all contestants who participated in the AppWorld Berlin iRules Contest. Your creativity, innovation, and willingness to share your ideas continue to push the community forward. Thank you to our judges: John_Alam Joel_Moses Moe_Jartin Chris_Miller Michael_Waechter dennypayne Kevin_Stewart Marcus-f5 SimonKowallik Sorin_Boiangiu Steve Scott Thank you to the DevCentral community. We learn together, grow together, and inspire each other every day. What's Next? Innovation and Creativity have been a key part of the contest rubric. We’re leaning into it, too. As we plan more contests for the year, we’re looking beyond iRules, with potential to expand to all programmability. The future is coming for us all; let’s greet it and move forward together.534Views6likes2CommentsLayered Virtual Server iRule Solution for ICAP File Upload Scanning on BIG-IP
Problem Our client is running a WebApp where his customers are able to upload documents. A BIG-IP Cluster is used to balance the WebApp. Our client wanted to scan the files via an existing ICAP Solution. After several tests with the standard ICAP and Request Adapt solution we noticed the application workflow breaks when a virus is detected and the Upload is not completed. From troubleshooting with the client we narrowed down the root cause to the ADAPT Profile returning "respond". The BIG-IP sends this to the customer endpoint and no response is sent to the backend Server, which then breaks the workflow for the Upload. Solution With the root cause found, we implemented a layered approach. We configured two virtual servers. The first Virtual Server acts as the outer layer. In the initial request is processed by an LTM Policy which checks if the request is a file upload. This sets a pointer for POST Requests to the Endpoint which triggers the Layered iRule processing. A GET Request is directly bypassed to the Content VS behind the outer layer. If a POST is received, we save the HTTP Request in "req_headers" and send the request to the Content VS. In the Content VS iRule the Request is again checked for POST or GET Requests and the ADAPT Profile is activated accordingly. If the ICAP Result is "respond" a custom response is crafted as HTTP 406 with an X-Virus-Found Header The responses are sent back trough the Layered VS. If the status code equals 406 and the Header X-Virus-Found is present the request is checked if it was already resend to the backend App. If it was not the HTTP::retry is used to resend the request to the backend, without the malicious content. Impact As the clients Web Application was old and there was no cost effective way to implement a workaround on Application side or the purchase a new ICAP Solution, the iRules combined with LTM Policy helped to client to scan the uploads for malicious content, keeping the App safe, while using existing technologies. Code iRule VS Layered (Outer Virtual Server) when RULE_INIT { # Set to "1" for debugging set static::debug 0 } when CLIENT_ACCEPTED { # Initialize the retry variable. It is required to resend the request. set retries 0 if { $static::debug } { log local0. "*** Retry set ***" } } when HTTP_REQUEST { if { $static::debug } { log local0. "*** Layered VS: Request received Number $retries ***" } # Entry condition from the LTM Policy and check of the file size if { ([info exists avscan]) and [HTTP::header Content-Length] < 10000000 } { if { $static::debug } { log local0. "*** Layered VS: Found POST ***" } if { $static::debug } { log local0. "*** Layered VS: Content-Length is [HTTP::header Content-Length] ***" } # Check whether this is a retried request if { $retries == 1 } { # If the request is retried, remove the Content-Length header # so that an empty POST is sent HTTP::header remove Content-Length if { $static::debug } { log local0. "*** Retried Request: Content-Length Header removed ***" } } # Store all request headers in a variable. # This variable is needed later for the retry. set req_headers [HTTP::request] if { $static::debug } { log local0. "*** Layered VS: Got Request $req_headers ***" } # Forward to the actual virtual server with the pool virtual icap_content_vs } # Handle all other requests, e.g. GET, by sending them directly to the VS with the pool else { if { $static::debug } { log local0. "*** Layered VS: Found GET ***" } virtual icap_content_vs } } when HTTP_RESPONSE { if { $static::debug } { log local0. "*** Layered VS: Response Received ***" } # Check whether the server response came from the iRule on the Content VS if { [HTTP::status] equals "406" and [HTTP::header exists "X-Virus-Found"] } { if { $static::debug } { log local0. "*** Layered VS: Found Virus ***" } if { $static::debug } { log local0. "*** Layered VS: Request Headers are $req_headers ***" } # If the retry counter is 0, resend the request, but without content if { $retries == 0 } { if { $static::debug } { log local0. "*** Retrying Request ***" } HTTP::retry $req_headers incr retries return } } # Reset state after response processing set retries 0 unset req_headers } Content VS iRule when RULE_INIT { # Set to "1" for debugging set static::debug 0 } when HTTP_REQUEST { if { $static::debug } { log local0. "*** Content VS: Request Received ***" } # Entry condition from the LTM Policy and check of the file size if { ([info exists avscan])and [HTTP::header exists Content-Length] and [HTTP::header Content-Length] < 10000000 } { if { $static::debug } { log local0. "*** File found ***" } if { $static::debug } { log local0. "*** Content-Length is [HTTP::header Content-Length] ***" } # Enable the ADAPT profile to access the internal virtual server ADAPT::enable enable } else { ADAPT::enable disable } } when ADAPT_REQUEST_RESULT { if { $static::debug } { log local0. "*** ADAPT Result is: [ADAPT::result] ***" } # Check the result returned by the ICAP server (respond case) if { [ADAPT::result] contains "respond" } { if { $static::debug } { log local0. "*** Modified ADAPT Result is: [ADAPT::result] ***" } # If the ICAP return value indicates that a virus was detected, # send a manual response and trigger the retry function # in the ir_AVScan_Layered iRule HTTP::respond 406 -version auto X-Virus-Found "Virus" } } Demo Would habe loved to create a demo. Unfortunately I have no access to the App.150Views0likes1CommentGeneric iRule based on datagroup parsing
The creation of this iRule comes from a migration project from Apache configuration to F5 Big IP. Different constraints lead to this approach of storing the configuration elements from the Apache conf in a datagroup that is then parsed by this iRule to dynamically derive the rules to apply to traffic. These are some simple or complex rules, but they are all uniformly stored in the datagroup, that can be modified by non-F5 friendly persons without impacting the rest of the configuration.291Views5likes3CommentsSUPER-WEBSOCKET-HANDSHAKE-LOGGER™® (SWHL) iRule
The contest submission covers a so called SUPER-WEBSOCKET-HANDSHAKE-LOGGER™® (SWHL) iRule. The genius idea behind this iRule is to log and correlate every single WEBSOCKET-Handshakes via the WS_REQUEST and WS_RESPONSE events. The iRule uses a well-selected iRule syntax and it has been carefully tested on TMOS v16, v17 and v21 units. How to use: Save the iRule to your device. Attach it to your virtual server. Adjust the $static::super_websocket_handshake_logger(DEBUG_SOURCE) variable to match your client-ip address or client-subnet. Perform websocket request. Open your BAS and type: ~# tail -f /var/log/ltm | grep "SUPER-WEBSOCKET-HANDSHAKE-LOGGER" Enjoy the lovely iRule! when RULE_INIT { # SUPER-WEBSOCKET-HANDSHAKE-LOGGER iRule by Kai Wilke set static::super_websocket_handshake_logger(DEBUG_SOURCE) "10.11.12.0/24" ;# CIDR-Notation } when WS_REQUEST { set swl_requestID "" if { [IP::addr [IP::client_addr] equals $static::super_websocket_handshake_logger(DEBUG_SOURCE)] == 0 } then { return } set swl_requestID "[clock clicks][TMM::cmp_unit]" log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::client_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port] | WS-REQUEST | [set httpRequest "[HTTP::method] [HTTP::host][HTTP::uri]"]" foreach header [HTTP::header names] { log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::client_addr]:[TCP::client_port] -> [IP::local_addr]:[TCP::local_port] | WS-REQUEST-HEADER | $header: [HTTP::header value $header]" } } when WS_RESPONSE { if { $swl_requestID eq "" } then { return } log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::local_addr]:[TCP::local_port] -> [IP::client_addr]:[TCP::client_port] | WS-RESPONSE | $httpRequest" foreach header [HTTP::header names] { log -noname local0.debug "SUPER-WEBSOCKET-HANDSHAKE-LOGGER | $swl_requestID | [IP::local_addr]:[TCP::local_port] -> [IP::client_addr]:[TCP::client_port] | WS-RESPONSE-HEADER | $header: [HTTP::header value $header]" } } Cheers, Kai147Views1like0Comments