irules
20648 TopicsAn Irule for Client Ssl Profile that Allows Unassigned TLS Extension Values (17516)
Hello Community, I have a requirement to allow enriched https header enrichment. The SSL negotiation (I'm doing ssl termination on F5) fails because the enriched header from client contains reserved tls extension values. (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtmltls-extensiontype-values-1). The Client Hello request in the SSL Handshake was captured and contained an Extensions list, which included a reserved TLS Extension value (17156), which the F5 isn't presenting in Server Hello. I need an irule that can allow that Extension to be added on the client ssl profile so the ssl handshake doesn't fail.3.2KViews0likes27CommentsFingerprinting TLS Clients with JA4 on F5 BIG-IP
JA4+ is a set of simple network fingerprints that are both human and machine readable to facilitate more effective threat-hunting and analysis. In this article you will learn how you can use F5 iRules to gerenate JA4 TLS fingerprints.6.8KViews11likes1Commentlog local0. is not a debugging strategy!
But let's be honest...with iRules, it's pretty much all we've had. If you have ever debugged an iRule, you know the ritual. Former F5er Jibin_Han in an article once called the log command is "the crudest of debug facilities." He was not wrong. It is the printf of application delivery, and at high traffic the logging pipeline will happily truncate your output just to keep things spicy. So back in TMOS 13.1, he shipped something much better: ltm rule-profiler. It is a passive tracer baked right into TMM. (Did you know this? Most don't, including most F5ers I talk to!) You do not touch your script. You tell it which virtual server, events, and occurrences to watch, you flip it on, and it emits a timestamped execution trace of everything your iRule did. Events firing, rules matching, the Tcl VM grinding through bytecode, native commands getting dispatched, variables changing. The whole shebang. There is just one catch. Ok actually two. It is tmsh-only. No GUI, no REST endpoint, no nothing. You configure it by hand and you start and stop it by hand. The not so fun part, the output looks like this: 1780079189187194,RP_EVENT_ENTRY,/Common/testvip-http,CLIENT_ACCEPTED,22623,0x70373707000576,10.1.10.6,36086,0,10.1.10.50,80,0 1780079189187210,RP_RULE_ENTRY,/Common/testvip-http,/Common/myrule,22623,0x70373707000576,... 1780079189187225,RP_RULE_VM_ENTRY,... 1780079189187240,RP_CMD_BYTECODE,/Common/testvip-http,push1,... Now multiply that by a few hundred lines, sprinkle in microsecond timestamps you are expected to subtract in your head, and remember that every single custom iRules command is actually a round trip out of the Tcl VM and back into TMM. A command, inside a VM, inside the microkernel. We need to go deeper. Reading it raw is less "performance analysis" and more "staring at the green rain in The Matrix and pretending you can see the woman in the red dress." The data is genuinely great. It's just wearing a CSV trench coat and refusing to make eye contact. But that ends now. Let's talk about Rültracer. What Rültracer is Rültracer is an iApps LX extension that gives ltm rule-profiler the face it always deserved. When Jibin_Han released his 3-part article series (linked at the bottom of this article) introducing the rule-profiler, we had a couple interns who built an analysis engine called Campfire that used a perl-based flamegraph package to display the trace in context of the "lift" of each occurence within a trace. The challenge was you had to manually configure everything on BIG-IP, send the logs somewhere, collect those logs, then import them into where you built campfire to run. A lot of manual work to get to the good. But Rültracer? It runs on the BIG-IP. An on-box Node worker handles the unglamorous parts: it configures the profiler, sets up (and tears down) the log publisher, captures the trace stream into a per-session file, and serves that file to a browser app. The browser does all the parsing and visualization client side, so the box just ships raw CSV and gets out of the way. What you get: A sequence diagram and step-through debugger. The trace becomes a UML-style sequence diagram across six lifelines (Users, Event, Rule, Rule VM, Command VM, Command), with the TMM and Tcl VM sides color coded so you can actually see every handoff between the microkernel and the interpreter. Which matters, because that back-and-forth is exactly where iRule inefficiency likes to hide. Next to it sits a linked step-through with a timeline scrubber, variable state, and command replay. Click the diagram, the table follows. Click the table, the diagram follows. There's a Tclsh disassembler you can enable as well, and as long as you're willing to modify the parts of the iRules code under test. I wrote an article years ago on this disassembly functionality, it's worth the read to see what this functionality affords you. iRule source mapping. It pulls your actual iRule source and lights it up: which commands fired (with microseconds and counts), which branches never ran, and which lines were ambiguous multi-matches. Your code, annotated by what the trace really did. (this part is early stages, it needs work.) Flamegraphs, with diff. An interactive flamegraph where width equals inclusive time, so the widest frame is your prime suspect. Find the slow command without playing Where's Waldo. Captured a "before" and an "after"? The diff view paints frames red and blue by how their self-time shifted, so you can prove your optimization actually optimized. Cycles versus CPU. Rültracer takes the box's own ltm rule stats hardware cycle counters and turns them into honest performance tables: cycles to microseconds, percent of a CPU per request, and max requests per second before your iRule becomes the bottleneck. It even reconciles the authoritative cycle counts against the trace-derived numbers, and the gap between them is the profiler's own overhead. These are numbers you can bring to a capacity-planning meeting without getting laughed out of the room. This is based on Deb Allen's yesteryear work in excel for computing capacity with iRules and my update in doing this with python much later. Reports and exports. Export a capture as self-contained HTML, JSON, a Mermaid sequence diagram, or Brendan Gregg folded stacks. Share it, attach it to a ticket, or feed it to your own tooling. Multi-TMM aware. Captures that span multiple TMMs get partitioned by context id with a scope selector, because of course your traffic did not politely land on a single TMM. No (post RPM install) build steps, no cloud, no telemetry, no agent. Vanilla JS in the browser, a small ES5 worker on the box, fully self-contained. And it is a lab tool on purpose: tracing adds significant TMM overhead, so this is not something you run in production. Rültracer tears the profiler and publisher down for you on teardown, so you never accidentally leave the tap open. Installing it Rültracer installs over SSH. You build the RPM on your workstation, copy it to the BIG-IP, and run the installer on the box as root. The installer provisions the persistent data directory, installs the package through the iApps LX framework, runs the post-install step, and confirms the workers came up. Replace and with your BIG-IP's SSH host and port. If it answers on plain old port 22, drop the -P / -p flags. First time on a fresh box The installer script lives outside the RPM, so it rides along once: Copy the installer on the box (one time only) scp -O -P <port> build/install-onbox.sh root@<host>:/shared/images/ Build, ship, and install ./build/build-rpm.sh 0.7.1 0001 scp -O -P <port> build/dist/rultracer-0.7.1-0001.noarch.rpm root@<host>:/shared/images/ ssh -p <port> root@<host> /shared/images/install-onbox.sh 0.7.1-0001 Because the installer runs as root, it creates the session data directory owned by the restnoded worker user before the workers start (the worker is uid 198 and cannot create directories under /shared/ on its own). When it finishes, it prints your UI URL: https://BIG-IP-host/mgmt/shared/rultracer/ui/ Open that, and you are in. Follow-on updates The installer is already on the box, so the next rounds are just bump, build, ship, install like above. That is an in-place upgrade and it keeps your saved sessions. If you ever want a clean slate, pass --reinstall, but note it wipes session data, so hit the Sessions tab's "Download backup" button first if you care about what is in there. See it in action (The walkthrough covers a live capture end to end: pointing the profiler at a virtual server, driving a little traffic, then digging through the sequence diagram, flamegraph, and cycle stats on a real trace.) Conclusion This was a fun project to bring together something I've played with a lot since the v13.1 release but could never quite figure out a packaging solution to make it functional enough to rely on. You can find the code in the Rültracer repo on Github. Let me know in the comments if you take a look at this and submit any bugs ore feature requests as an issue out on Github.
170Views2likes0CommentsAPM SAML IdP - SP Issuer Extraction
Problem this snippet solves: APM doesn't expose any detail about the SAML SP Issuer when authentication requests hitting APM as an IdP during an SP initiated SAMLRequest. This iRule when applied to a SAML IdP enabled virtual server will extract the assertion request, decode it and present the SAML SP Issuer ID as the session variable %{session.saml.request.issuer} within APM. How to use this snippet: This comes in real handy when performing authorisation of the resource and could help avoid having APM perform a TCP connection reset when a SAML resource isn't authorised. Code : when CLIENT_ACCEPTED { ACCESS::restrict_irule_events disable } when HTTP_REQUEST { if { [HTTP::path] equals "/saml/idp/profile/redirectorpost/sso" } { if { [HTTP::method] equals "POST" } { # Colelct POST data set content_length [HTTP::header value Content-Length] HTTP::collect $content_length } elseif { [HTTP::method] equals "GET" } { #TODO } } } when HTTP_REQUEST_DATA { set payload_data [URI::decode [HTTP::payload]] log local0. "payload=[URI::query "?$payload_data" "SAMLRequest"]" if { $payload_data contains "SAMLRequest" } { # Extract SAML request data set SAMLdata [b64decode [URI::query "?$payload_data" "SAMLRequest"]] set SAML_Issuer_loc [string first "saml:issuer" [string tolower $SAMLdata]] set SAML_Issuer_start [expr {[string first ">" $SAMLdata $SAML_Issuer_loc] + 1}] set SAML_Issuer_end [expr {[string first "<" $SAMLdata $SAML_Issuer_start] - 1}] set SAML_Issuer [string range $SAMLdata $SAML_Issuer_start $SAML_Issuer_end] if { !([ACCESS::session sid] equals "" ) } { ACCESS::session data set session.saml.request.issuer $SAML_Issuer } } } when ACCESS_SESSION_STARTED { if { [info exists SAML_Issuer] } { ACCESS::session data set session.saml.request.issuer $SAML_Issuer } } Tested this on version: 11.61.7KViews2likes8CommentsAppWorld LATAM 2026 - "Write Your First iRule" Contest
Este anuncio ha sido actualizado para incluir una traducción al español. Puede encontrar las instrucciones en español al final de la publicación. 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! AppWorld LATAM 2026 - Concurso “Escribe tu Primer iRule” ¡Hola querida comunidad! El Concurso de iRules está de regreso, con un nuevo estilo. Los iRules de Las Vegas y Berlín demostraron una experiencia increíble. Para este tercer Concurso de iRules, cambiaremos el enfoque hacia el estímulo y la educación con el tema: Concurso Comunitario “Escribe tu Primer iRule”. Estamos desafiando a los miembros de la comunidad DevCentral que asistan a AppWorld LATAM 2026 a diseñar y construir un iRule en un entorno acogedor. Ya sea que estés escribiendo un iRule por primera vez, o apenas estés encontrando tu ritmo, no podemos esperar a ver lo que crees. (Y no te preocupes, no tiene que ser literalmente tu primer iRule. Lo que cuenta es el espíritu de intentar algo nuevo.) El Desafío Planea y escribe un iRule que aborde un caso de uso de las capacidades de BIG-IP. Puedes: Crear un nuevo iRule Reimaginar los iRules existentes del codeshare de DevCentral Adaptar un iRule de 20 líneas o menos del GitHub iRules Toolbox Valoramos tu perspectiva fresca y tu mirada renovada. Como es una oportunidad de aprendizaje, también te animamos a divertirte con ello. Premios Las presentaciones serán evaluadas para los premios por categoría. Todos los participantes reciben una camiseta exclusiva del concurso. Puesto Premio Premios por Categoría $200/cada uno Premio a la Excelencia Técnica $500 Participación camiseta ¿Qué Hace a una Entrada Ganadora? Los criterios de evaluación de 100 puntos para las presentaciones se definen a continuación en cuatro categorías: Excelencia Técnica (25 puntos) ¿Está bien construido y listo para producción? Considera: Funciona correctamente Consciente del rendimiento (eficiente, impacto mínimo en recursos) Sigue las mejores prácticas de seguridad Código limpio y legible Impacto en el Usuario (25 puntos) ¿Tú y otros usuarios realmente lo usarían? Considera: Resuelve un problema operativo real o una necesidad técnica Aplicabilidad práctica y potencial de adopción Valor de negocio claro Documentación exhaustiva Innovación y Creatividad (25 puntos) ¿Esta solución muestra un pensamiento original? Considera: Perspectiva fresca sobre desafíos comunes Enfoque único para resolver un problema moderno ¿Inspira colaboración y progreso? Tema y Alineación (25 puntos) ¿Este iRule refleja tu aprendizaje de AppWorld LATAM 2026 y de los recursos de la comunidad? Considera: Aplicar el conocimiento y las habilidades que has aprendido Accesible para otros nuevos escritores de iRules Demuestra tu esfuerzo por intentar algo nuevo para ti Fechas Importantes Apertura del Concurso: 8 de junio de 2026 a las 12:00 a.m. Hora del Pacífico Fecha Límite de Presentación: 31 de julio de 2026 a las 11:59 p.m. Hora del Pacífico Anuncio de Ganadores: 14 de agosto de 2026 Cómo Participar El concurso está abierto a todos los socios y clientes de F5, y miembros de DevCentral que estén registrados y asistan al concurso en AppWorld LATAM 2026, excepto como se describe en las Reglas Oficiales. Por favor consulta las Reglas Oficiales para los términos completos, incluidas las condiciones de participación y elegibilidad. Regístrate en DevCentral y únete al grupo Community Contests. Busca a Hannah o Buu en el área de la Comunidad si necesitas ayuda. Construye y envía antes de las 11:59 p.m. Hora del Pacífico del 31 de JULIO de 2026. Edita tu borrador tanto como quieras, pero una vez que lo envíes, eso es lo que revisaremos. Aquí tienes un ejemplo de entrada anclado al inicio de la página de Contest Entries que deberías seguir. Asegúrate de agregar estas etiquetas a tu entrada: “appworld 2026”, “latam” e “irules” como se muestra en ese ejemplo. IMPORTANTE - Necesitas unirte al grupo Contests para enviar tu entrada. ¿Nuevo en iRules? ¡Perfecto! Damos la bienvenida a participantes de todos los niveles de habilidad. Si recién estás comenzando, consulta nuestra guía Getting Started with iRules: Basic Concepts. Este concurso es una gran oportunidad para aprender haciendo. Siéntete libre de traer a tus colegas favoritos y a tus compañeros de IA para ayudarte a crear tu entrada. Reflexiones Finales Publica todas tus preguntas relacionadas con el concurso en los comentarios a continuación. El Concurso de iRules tiene una rica historia de hacer emerger soluciones creativas desde la comunidad. Abordar los problemas de manera diferente inspira algunas de las mejores ideas que hemos visto. Esperamos con ansias ver y celebrar lo que construyas. Apréndelo. Constrúyelo. Compártelo.258Views1like0CommentsExplicit write control for iRules subtables
Note to the reader...apparently what is old is new again. There are some threads here on DevCentral that have already solved for this, albeit in different ways. The few brought to my attention by MVP Kai_Wilke are included in the list below for your benefit to read through. That said, the journey of discovery here in this article is worth your time to understand the nuances of how data is passed in a multi-TMM system. Dealing with iRule $variables for HTTP2 workload while HTTP MRF Router is enabled | DevCentral https://github.com/KaiWilke/F5-iRule-RADIUS-Server-Stack SPDY/HTTP2 Profile Impact on Variable Use | DevCentral The TL;DR TMM subtables on BIG-IP are partitioned across TMMs by hashing the subtable name. Writing to a subtable from a non-owner TMM is roughly 1000x slower than writing from the owner...single-digit clock clicks vs. tens of thousands. If you want fast per-TMM local storage, you cannot pick the subtable name yourself; you have to *discover* a locally-owned name by timing trial writes. Deterministic naming schemes do not work, even when they look obviously correct. The Problem A colleague had an iRule that maintained per-connection state across many CLIENT_DATA events. The natural data structure was a TMM session subtable. His quick experimenting showed the writes were slow enough to push the system CPU under modest load and needed to understand why before scaling further. There's an example proc library from Nat_Thirasuttakorn "LOCALDB" that uses a clever timing trick: it generates a random subtable name, times a probe write, and only keeps the name if the write completes under some threshold (50 clock clicks in the original). The implication was that most random names produce slow writes and only a few are fast. I read the code, figured I understood it, and rewrote it "cleanly" using deterministic per-TMM names: `localdb_tmm_0`, `localdb_tmm_1`, `localdb_tmm_2`, ... one per TMM, no probing required. Each TMM would write only to its own name. Done, right? Wrong. The diagram above is the mental model the rest of this post leans on. Two independent hashes are happening: the DAG hashes the inbound 4-tuple to choose which TMM accepts the connection, and TMOS separately hashes the subtable name to choose which TMM *owns* the storage for that name. A write succeeds only when both hashes agree; when the TMM that received the connection is also the owner of the subtable being written to. When they disagree, the write costs roughly 7000x more. The Investigation The deterministic version "worked" — writes succeeded, distribution looked plausible, throughput was decent. Then I added timing instrumentation per TMM and looked at the percentiles: TMM samples min avg max 0 74 121 64855.6 229089 1 34 136 71536.3 236204 2 38 121 88516.9 293259 3 62 3 13.3 25 TMM 3 was writing in 3-25 clicks. Every other TMM was averaging tens of thousands, which is a 5,000-7,000x gap! Something was very wrong. The diagnosis came from a `/probe` endpoint I'd added for unrelated reasons: hit the same subtable name from many connections, time each write, count which TMM responds fast. Probing each of the four "deterministic" names produced: localdb_tmm_0 → owner is TMM 2 localdb_tmm_1 → owner is TMM 2 localdb_tmm_2 → owner is TMM 3 localdb_tmm_3 → owner is TMM 3 Visualizing the result for one of those probes makes the signal unambiguous: Two of the four names hashed to TMM 2, the other two hashed to TMM 3. TMMs 0 and 1 didn't own any of the subtables I'd "assigned" to them. This is the key insight: **the subtable name `localdb_tmm_3` doesn't get owned by TMM 3 just because its name ends in 3.** TMOS hashes the whole name string and assigns ownership based on that hash. The hash is opaque, and it's stable, but it has no relationship to the content of the name. My deterministic scheme was generating four unique names, which guaranteed no key collisions across TMMs — but it didn't guarantee, and couldn't guarantee, that name N landed on TMM N. Why The Original Trick Was Right Going back to the LOCALDB proc library pattern from DevCentral: while { $try < $maxtry } { set name [expr rand()] set before [clock clicks] table set -subtable $name test_$name $name 5 set after [clock clicks] set diff [expr {$after - $before}] if { $diff < $maxdiff } { break } incr try } Generate a random name. Probe it. If it's fast, keep it; if not, throw it away and try another. Each TMM independently does this, and on average needs ~N tries on an N-TMM system to find a name it owns. The probe is the *only* reliable way to know. The randomness is load-bearing. The timing measurement is load-bearing. Neither is decorative. My "elegant" rewrite removed both and produced a system that looked fine but was burning 99% of its potential throughput shipping writes between TMMs. How to Verify A timing histogram per TMM is the diagnostic. The test workflow: Add a `/probe?name=X` endpoint that times a single `table set` against an arbitrary subtable name and reports clicks + the responding TMM Hit it many times from a multi-threaded client Aggregate per-TMM: hits, OWNER count (writes under threshold), NON_OWNER count, min/avg/max clicks The owner of name X will show up as ~all-OWNER with consistently low clicks; everyone else shows ~all-NON_OWNER with high clicks A handful of stray "OWNER" tags on non-owners is just noisy variance in `clock clicks` measurement. The real signal is overwhelming: 50+ OWNER tags vs 0-3 OWNER tags, and average clicks differing by 1000-10000x. Lessons About TMM Subtables A few things worth internalizing if you work with these: Names are global; storage is partitioned Two TMMs writing the same name reach the same logical subtable, but only the owner stores it locally. Non-owners pay an inter-TMM coordination tax on every operation. This is fundamentally a sharding scheme where the shard key is the subtable name and the shard map is hidden from you. Construction can't replace discovery Anywhere a system uses an opaque hash to assign ownership of named resources, you cannot construct a locally-owned name, you can only find one by trying. This pattern shows up well beyond TMOS: Cassandra token ranges, Redis Cluster slots, Kafka partition assignments, consistent-hashing rings in general. Discovery beats construction whenever the mapping function is hidden. O(n) reads in hot paths kill throughput I had a `count` proc that called `table keys -subtable X` and ran `llength` on the result. With per-TMM subtables of ~25k entries, that's 25k strings to enumerate per request. Throughput decayed from 3300/s to 600/s over a 40k-record run, a perfect 1/n curve. Maintaining the count incrementally in a `static::` variable made it O(1) and throughput stayed flat. The fix is obvious in hindsight; the bug is invisible without per-second throughput measurement. Static variables are per-TMM This is great when you want it (per-TMM owned-subtable name, per-TMM counters) and confusing when you don't (you can't share state across TMMs through statics alone). The variables are also persistent across rule reloads in some versions, which means a rule update that adds a new static can leave you with TMMs running the new code but missing the new state. Defensive existence checks at the top of every proc are worthwhile. Sampling debug logs is mandatory at scale Logging every write to `/var/log/ltm` for a million-record load is 1M log lines, hundreds of MB, and enough log I/O to tank throughput on its own. Sample 1-in-N (where N grows with load size), and gate calling-rule logs on the same sample point so the log narrative stays coherent. A `should_log` helper proc shared between the library and its callers keeps this clean. Test harnesses should reset, not reload I initially "reset" between runs by reloading the iRule. `RULE_INIT` re-ran and statics reset, but the *subtable contents* persisted in TMM session memory because they're indexed by name, not by rule. Each rule reload picked a new random name and orphaned the old subtable's entries. Over many runs, memory accumulated. A `/reset` endpoint that walks `table keys` and deletes them is the right abstraction. What "Done" Looked Like After the fix, a 100k-record run on a 4-TMM system: TMM samples min avg max 0 98 3 17.4 71 1 101 4 18.9 88 2 99 3 16.8 77 3 102 4 19.1 91 Throughput stayed flat at ~3000/s for the entire run. Every TMM in the same low-clicks range. No `SLOW` tags in the sampled logs. The before-and-after chart (log scale) makes the impact unmistakable: TMM 3 is interesting on its own. Under the broken design it was already fast (averaging 13.3 clicks) because the deterministic names happened to hash to it, meaning every other TMM was ferrying its writes over to TMM 3. Under the fix, TMM 3 stops being a single hot point and instead does roughly the same work as everyone else, on its own subtable. The fact that TMM 3's "broken" bar isn't dramatically taller is what makes this kind of bug survive a smoke test: writes were succeeding, throughput looked plausible, *one* TMM was even fast. The percentile breakdown is what gave it away. The Validated Test Session Here is the actual end-to-end verification run, command by command, on a 4-TMM lab BIG-IP. This is the workflow that I ended up codifying in the project's `USAGE.md` — it both validates that the fix works and demonstrates each tool's role. Step 1: Verify Every TMM Picked a Unique Subtable After deploying the LOCALDB rule and the calling rule, hit `/whoami` enough times that fresh TCP connections fan out across all TMMs: $ for i in $(seq 1 30); do curl -s http://10.0.2.49/whoami; done | sort -u tmm 0 subtable localdb_tmm_0_865802 total_tmms 4 writes 0 entries 0 tmm 1 subtable localdb_tmm_1_922743 total_tmms 4 writes 0 entries 0 tmm 2 subtable localdb_tmm_2_5946 total_tmms 4 writes 0 entries 0 tmm 3 subtable localdb_tmm_3_441563 total_tmms 4 writes 0 entries 0 Four things to read out of this: Four unique TMMs (0, 1, 2, 3) responded meaning full coverage. With `Connection: close` from curl, each request gets a fresh ephemeral source port and the BIG-IP's DAG re-hashes; 30 requests against 4 TMMs is essentially guaranteed to hit all of them. Four unique subtable names, each with the responding TMM number as a prefix and a random suffix. The TMM-number prefix is just a label for human readability. The random suffix is what `init_table` actually iterates on during timing-probe discovery, throwing away names that hash to other TMMs and keeping the first one whose write completes under the threshold. `total_tmms=4` is consistent on every row. `TMM::cmp_count` is reporting the cluster size correctly. writes=0 entries=0` everywhere. Clean baseline before any load. Step 2: Reset to a Clean Baseline $ python tbl-loader.py reset --host 10.0.2.49 --port 80 Discovering TMM count from 10.0.2.49:80/info ... BIG-IP reports 4 TMMs. Sending 200 /reset requests with 32 workers... Reset summary: TMM hits first_deleted total_deleted ------------------------------------------ 0 50 0 0 1 47 0 0 2 55 0 0 3 48 0 0 All 4 TMMs cleared. Total entries removed (first-hit): 0 200 reset requests, 50 / 47 / 55 / 48 distribution across the four TMMs. That's essentially perfect uniform. Expected mean is 50, observed range is 47-55, which is well within the natural variance of a fair hash. Worth confirming because the same DAG is what'll spread the load run; uneven reset distribution would predict uneven load distribution, which complicates the analysis. `first_deleted=0` everywhere because the previous step's `whoami` had already shown empty subtables. After a load run, this column tells you exactly how many entries each TMM was holding. Step 3: Run the Load $ python tbl-loader.py load --host 10.0.2.49 --port 80 --count 100000 --workers 64 ... completed=100,000/100,000 (100.0%) rate=4376/s coverage=4/4 missing=[] errors=0 Done. completed=100,000 errors=0 elapsed=22.9s rate=4375/s Final distribution: tmm 0: 25,198 writes (25.20%) tmm 1: 24,782 writes (24.78%) tmm 2: 24,914 writes (24.91%) tmm 3: 25,106 writes (25.11%) Three numbers worth lingering on: Sustained 4,375/s throughput, completely flat Earlier in the project, before the O(1) `count` fix, the equivalent run started at 3,300/s and decayed to 600/s by the 40k-record mark, a perfect 1/n curve from the hidden `table keys` + `llength` cost in the calling rule. With `static::LOCALDB_entries` maintained incrementally, the per-write work is genuinely constant and throughput stays where it starts. Distribution within ±0.25% of perfect uniform 25.20% / 24.78% / 24.91% / 25.11% is what fair hashing produces over 100k samples. The DAG is doing its job; nothing is being funneled through one TMM the way the broken-locality version was. Zero errors over 100k fresh TCP connections No TIME_WAIT exhaustion on the client (the ephemeral port range is wide enough), no rate limiting on the BIG-IP, no socket timeouts. Suggests the workload is well within both ends' capacity. The 22.9 second elapsed time works out to ~5 microseconds per write end-to-end, including the full TCP setup/teardown for each request. The actual `table set` is in the tens of clock clicks (single-digit microseconds), so HTTP and TCP overhead dominate, which is the right answer when the iRule work itself is fast and local. Step 4: Verify Per-TMM Locality from the Logs The throughput and distribution numbers tell us writes are happening evenly, but they don't directly prove each write is *local*. For that, pull the sampled timing lines from the BIG-IP's log and run them through the analyzer. Filter to the test window so earlier (broken) runs don't pollute the stats: $ ssh [email protected] "grep '^May 6 16' /var/log/ltm | grep 'sampled'" \ | python3 timing_stats.py Sample rate: 1/1000 Locality threshold: 100 clicks TMM n FAST SLOW min p50 avg p95 p99 max ------------------------------------------------------------------------------ 0 25 25 0 3 5 5.5 10 11 11 1 24 24 0 3 5 6.1 11 18 18 2 24 24 0 2 6 6.1 10 11 11 3 25 25 0 2 6 6.5 12 13 13 ------------------------------------------------------------------------------ Total: 98 samples across 4 TMMs FAST_LOCAL=98 SLOW=0 OK: all TMMs have average write timing below 100 clicks. Per-TMM locality is working. This is the centerpiece of the validation. Reading it line by line: Sample counts 25 / 24 / 24 / 25 samples per TMM matches the 25.20% / 24.78% / 24.91% / 25.11% write distribution from the load output, which is what you'd expect if the BIG-IP is logging 1-in-1000 of all writes uniformly. Timing Single-digit minimums (2-3 clicks). Averages of 5.5-6.5 clicks. p99s of 11-18. Max of 18 across all 98 samples. Compare to the broken run earlier in the project (shown at the top of the article in the investigation section), on the same hardware with the same workload but the wrong `init_table`. That's a **10,000x improvement on three of the four TMMs** between the two runs. The only thing that changed was `init_table` switching from deterministic naming to timing-probe discovery. Tag tally 98 FAST_LOCAL, 0 SLOW. Not a single sampled write missed the locality threshold. The 100-click threshold has plenty of headroom, the actual max was 18, an order of magnitude below. Verdict The script's automated check confirms locality is working. This is the line you'd grep for in CI if you wanted regression coverage. Step 5: Spot-Check Ownership of a Discovered Name The timing report proves writes were fast, but it doesn't prove that the *names* each TMM picked are actually owned by those TMMs (only that their writes were fast for whatever reason). To close that gap, take one of the names from `whoami` and probe it directly: $ python tbl-loader.py probe --host 10.0.2.49 --port 80 --name localdb_tmm_2_5946 --requests 200 ... Results for subtable 'localdb_tmm_2_5946': TMM hits OWNER NON_OWNER min_clicks avg_clicks max_clicks ---------------------------------------------------------------- 0 55 0 55 286 5139.9 19814 1 70 0 70 127 12475.3 52544 2 8 8 0 3 8.6 20 3 67 0 67 238 7126.6 51939 Likely owner of subtable 'localdb_tmm_2_5946': TMM 2 (avg 8.6 clicks, tagged OWNER 8 times) This is unambiguous: TMM 2 wrote in 3-20 clicks, average 8.6 Consistent with the 6.1 average from `timing_stats.py` during the load. Small differences, both well under threshold, both unambiguously local. TMMs 0, 1, 3 took 127-52,544 clicks, averages 5,139 / 12,475 / 7,126 Roughly 600x to 1,500x slower than TMM 2 on the same operation. They're paying the inter-TMM coordination tax because the subtable is owned by TMM 2. Zero stray OWNER tags on non-owning TMMs Earlier probe runs against fresh subtables sometimes had 1-3 stray OWNER tags from non-owners due to `clock clicks` jitter on small subtables. With this subtable now containing ~25k entries, the non-owner penalty is large enough (mins of 127-286 clicks) that no stray write made it under the 100-click threshold. The bigger the subtable, the cleaner the signal. TMM 2 only got 8 hits That's just sampling variance. The DAG hashed inbound connections 55 / 70 / 8 / 67, which over 200 requests is a normal-looking spread. With 1000 requests you'd see ~250 hits per TMM. The 8 hits TMM 2 did get were unanimous on OWNER, which is what matters. A run against any of the other discovered names (`localdb_tmm_0_865802`, `localdb_tmm_1_922743`, `localdb_tmm_3_441563`) produces the same shape of result with the corresponding TMM as owner. What This Validates Step 1 proves every TMM ran `init_table` and picked a unique name. Step 2 proves clean baseline and even DAG distribution. Step 3 proves throughput is sustained and writes spread evenly across TMMs at scale. Step 4 proves every write was fast at the time it happened. Step 5 proves the names each TMM picked are genuinely owned by those TMMs. Together they're a complete proof of the design: the timing-probe discovery in `init_table` correctly identifies a locally-owned subtable name on each TMM, and operations against those names cost ~10 clock clicks instead of ~70,000. The cost gap is the entire reason the per-TMM-subtable pattern exists, and it's now empirically demonstrated end-to-end. This validation run took maybe three minutes of wall time. It's the kind of verification I should have been running before believing the original "deterministic naming" rewrite worked, not after watching it fail under load. Pushing Throughput: Per-Write to Bulk-POST The validated workflow above writes one key per HTTP request. That's the right shape for testing locality (each write is a clean, isolated trial), but it makes TCP connection setup the dominant cost. At ~4,375 writes per second on a 4-TMM box, the iRule is spending most of its time accepting connections, parsing headers, and tearing down sockets, not writing to subtables. The natural next step is to batch many writes into a single HTTP request. A separate `/bulk_load` endpoint accepts a POST body of newline-separated keys (UUIDs in our test case), collects the body via `HTTP::collect`, and walks the lines in a tight loop calling `LOCALDB::set_unique` on each. One TCP connection now writes 15,625 keys instead of one. Per-batch timing comes back in the response so the loader can aggregate it client-side. The throughput result is striking: Same hardware, same iRule logic, same per-TMM locality — the 30× gap is purely TCP setup cost saved. The per-write timing inside the iRule barely changed (3-6 clicks per `LOCALDB::set_unique` either way), but the request-level overhead collapsed because we stopped paying it 1M times. A few things worth noting about this bulk path that aren't obvious: Locality holds inside the loop A `/bulk_load` request that lands on TMM 2 will do all 15,625 of its writes against TMM 2's local subtable. There's no opportunity for a single batch to "leak" writes to other TMMs, because the connection is pinned to one TMM by DAG and the subtable name is fixed by `static::LOCALDB_name`. So the locality verdict from the per-write test carries over without needing re-verification and the loader's per-batch `clicks_per_write` measurement confirms it stays in the 3-6 click range. DAG fan-out still distributes work With 64 fresh POSTs, each gets its own ephemeral source port, so the DAG hashes them across TMMs the same way it did with single-write requests. After enough batches, the per-TMM POST counts converge. In one of the runs, 4 TMMs each took exactly 16 of 64 POSTs. Body size matters for HTTP::collect The `/bulk_load` handler reads `Content-Length` and calls `HTTP::collect $cl` to buffer the entire body before processing. We cap at 16 MiB to protect TMM memory; that's plenty of headroom (~400k UUIDs per batch) but it's a real ceiling worth knowing about. The default of 15,625 UUIDs is ~580 KiB, which is well within bounds. An aside: log volume kills throughput at this rate Our first three bulk-post runs showed throughput drifting downward across consecutive runs...163k/s, then 129k/s, then 122k/s on the same hardware with no other state changes between them. The cause turned out to be the calling rule's logging itself. The `/bulk_load` and `/reset` handlers each had unconditional `log local0.` statements, producing 64 + 200 = 264 syslog writes per test cycle on top of the LOCALDB sample logs. After silencing those handlers (the response bodies already carried the per-batch timing data, so we lost no visibility), runs stabilized at ~133k writes/s ± 4% and survived 60-second sleeps with no warmup penalty. The lesson generalizes: at high write rates, the rule path needs to be quiet, not just "not chatty." Even gated log statements run their gate evaluation on every request, and unconditional ones write to syslog regardless of intent. When the per-write iRule cost is in the single-digit microseconds, *any* per-request work shows up. The rule of thumb that emerged: log statements that fire once per HTTP request are fine for diagnostics (`/probe`, `/whoami`) but should be sampled or removed entirely from the hot path (`/load`, `/bulk_load`, `/reset`). The loader can carry timing data back in response bodies and aggregate it client-side, which is both faster and more useful for analysis. Worth flagging that the absolute throughput numbers here (130-160k writes/s) reflect the test environment: a BIG-IP VE running on an Intel NUC under VMware, sharing the host with the load generator and other VMs. Those are not headroom numbers; they're contention-dominated. A 16-vCPU appliance without that contention should comfortably scale 5-10× from these figures, putting bulk-load throughput into the millions of writes per second on real hardware. The Code The updated `LOCALDB.tcl`, the test harness `subtable_test_updates.tcl`, the Python loader/prober/timing-analyzer, and the USAGE.md are all in the irules-subtable-discovery repo out on Github. Two key bits to study: The `init_table` proc that does the timing-probe discovery, including the fallback path that logs a WARNING and uses a slow name rather than failing silently when discovery exhausts its tries. The 200-try ceiling is sized for 16+ TMMs; on a 4-TMM box you'll typically find a local name in 1-3 tries. The `/probe` endpoint and the loader's `probe` mode. Together they let you take any subtable name and identify which TMM owns it in seconds. Worth keeping in your toolkit; it's the cleanest way I've found to interrogate TMOS's hash assignments. Closing Thoughts The whole episode reinforced something I keep relearning: when a working pattern looks weirdly complicated, the complications are usually load-bearing. The original LOCALDB rule looked over-engineered with its random names and timing probes and retry loops. It was actually exactly as engineered as it needed to be. My "cleaner" rewrite was simpler because I'd quietly assumed something untrue about how TMOS assigns ownership. The truth was readable from a 6-line timing report; I just hadn't generated one yet. If you're going to deviate from a working pattern, the deviation should be the thing you instrument first. Note: the original LocalDB proc library I built this from has been updated by the author in a couple different ways since I shared my work with him. I didn't fold that work in here, but I'll post those updates along with the original when I get permission to do so.145Views1like0CommentsEnhancing the F5 DoD Banner with EU CAPTCHA (Myra) & Sideband Validation
Features & Security Hardening (v1.3) Besides the frontend EU CAPTCHA integration, this iRule introduces several security hardening measures (P3–P9): Strict POST Enforcement: Banner acceptance is strictly limited to POST requests. Content-Security-Policy (CSP): Implements restrictive HTTP headers tailored to safely allow the CAPTCHA's verify.js script to execute. Overview and Benefits of Myra EU CAPTCHA Myra EU CAPTCHA is a bot protection solution developed in Germany. It is positioned as a sovereign, European, and 100% GDPR-compliant alternative to traditional American providers (such as Google reCAPTCHA or Cloudflare Turnstile, which are subject to the CLOUD Act). Key Features: Seamless User Experience (Zero-Click): Verification is performed automatically in the background using cryptographic calculations. The user does not need to take any action (no visual puzzles to solve, no traffic lights to identify). The system is therefore completely accessible and barrier-free. Privacy by Design: The solution operates entirely without cookies and does not use any persistent storage in the browser (meaning you do not need to add a cookie consent banner). Furthermore, no personal data is stored, and any potentially identifying metadata is anonymized using a hashing system. Security and Sovereignty: Built on Myra Security technology, the solution relies on the analysis of over 100 billion daily CDN signals. Customers benefit from a certified sovereign technology (ISO 27001, BSI C5, PCI DSS) and can mandate that data processing takes place exclusively in data centers located in Germany or within the European Union. https://www.eu-captcha.eu/ https://docs.eu-captcha.eu/integration/frontend/html-integration/ Implementation Steps 1. Configure the Internal Sideband Virtual Server The iRule requires an internal Virtual Server to route API traffic to Myra. Create a file named eucaptcha-sideband-vs.conf in /var/tmp/ on your BIG-IP with the following content: ltm node /Common/node-api.eu-captcha.eu { fqdn { autopopulate enabled interval ttl name api.eu-captcha.eu } } ltm monitor https /Common/hm_myra_eucaptcha_https { adaptive disabled defaults-from /Common/https destination *:* interval 20 ip-dscp 0 recv HTTP/1 recv-disable none send "GET / HTTP/1.1\r\nHost: api.eu-captcha.eu\r\nConnection: close\r\n\r\n" time-until-up 0 timeout 21 } ltm pool /Common/pool_eucaptcha_api { members { /Common/node-api.eu-captcha.eu:https { } } monitor /Common/hm_myra_eucaptcha_https } ltm profile server-ssl /Common/server-api.eu-captcha.eu { app-service none defaults-from /Common/serverssl server-name api.eu-captcha.eu sni-default true } ltm virtual /Common/vs_dod_eucaptcha_sb { destination 10.10.10.8:webcache ip-protocol tcp mask 255.255.255.255 pool /Common/pool_eucaptcha_api profiles { /Common/http { } /Common/server-api.eu-captcha.eu { context serverside } /Common/tcp { } } serverssl-use-sni enabled source 0.0.0.0/0 source-address-translation { type automap } translate-address enabled translate-port enabled } Note: This configuration creates the necessary node, pool, Server SSL profile (with SNI enabled), and the internal Virtual Server vs_dod_eucaptcha_sb Merge this configuration into your BIG-IP via tmsh: tmsh load sys config merge file /var/tmp/eucaptcha-sideband-vs.conf 2. Generate Your Local HMAC Key To ensure the integrity of the banner acceptance cookie, generate a robust random string on your BIG-IP bash shell: openssl rand -hex 32 3. Deploy the iRule Create a new iRule on your BIG-IP and paste the source code. You must update the RULE_INIT block with your specific values: when RULE_INIT { set static::dod_banner_ttl 600 set static::dod_banner_hmac_key "PASTE_YOUR_OPENSSL_HEX_KEY_HERE" set static::eucaptcha_sitekey "YOUR_EUCAPTCHA_SITEKEY" set static::eucaptcha_secret "YOUR_EUCAPTCHA_SECRET" set static::eucaptcha_sideband_vs "vs_dod_eucaptcha_sb" # Trusted proxy CIDRs (empty = IP::client_addr only). Ex. : list "10.0.0.0/8" set static::dod_banner_trusted_proxy_cidrs [list] # Max acceptations / IP / window (seconds) set static::dod_banner_accept_rate_limit 10 set static::dod_banner_accept_rate_window 60 } Note: The HTML payload is embedded in the iRule and handles the frontend display and token submission. 4. Attach to your Virtual Server Attach the iRule to the public-facing Virtual Server protecting your application. Validation & Testing Open an Incognito/Private browsing window and navigate to your application. You should be intercepted by the DoD warning banner containing the EU CAPTCHA widget. Solve the challenge. The "I Accept" (J'ai pris connaissance et j'accepte) button will enable. Submit the form. The BIG-IP will validate the token via sideband, generate an HMAC-signed _si_gate cookie, and redirect you to the application. Refresh the page; the banner should be bypassed as long as the cookie remains valid (default 600 seconds). Credits Special thanks to Eric Haupt for the original repository that made this possible: https://github.com/hauptem/F5-LTM-iRule-based-DoD-Banners214Views0likes0CommentsIntroducing Rülbased - version your iRules on BIG-IP!
For all the BIG-IP maintainers out there who just don't have a centralized version control system for your iRules...this one's for you! The TL;DR Rülbased is an iApps LX extension that adds version control, change tracking, editing, and rollback capabilities to iRules on a BIG-IP. It lives on the device, watches for changes (whether made through the BIG-IP GUI, tmsh, iControl REST, ConfigSync, or Rülbased itself), captures every edit as a versioned snapshot with author and reason metadata, and lets you diff, restore, or audit any iRule's history without leaving the BIG-IP. Think of it as git log and git diff for iRules, with no external dependencies. Executive Summary Rülbased solves a problem most BIG-IP shops have lived with for years: iRules change, sometimes in ways no one remembers, and there's no built-in mechanism to see who changed what, when, or why. The BIG-IP audit log tells you something happened; it doesn't show you the code before and after, and it can't roll you back. Rülbased is a self-contained iApps LX RPM that installs via an iControl REST call and adds: Automatic baseline snapshot of every iRule on the device at install time, so history starts populated rather than empty Continuous change detection via a background poll worker. Edits made outside Rülbased (the BIG-IP GUI, tmsh, ConfigSync replication from a peer) are captured, hashed, and stored within minutes Per-edit metadata when changes go through Rülbased's own GUI: an author name and a free-text reason field, so every audit-log entry answers "why" Content-addressed version store with SHA-1 deduplication, so reverting to last week's working version doesn't take any more space than a regular snapshot Side-by-side and unified diff views between any two versions of any iRule, rendered in-browser with no external tooling One-click rollback to any prior version, with the restoration itself recorded as a new audit entry Syslog and webhook notifications on every change (including HMAC-signed webhook payloads) so changes flow into whatever SIEM, chat tool, or pipeline you already run Append-only audit log in JSON Lines format, queryable by rule, author, time window, or action type Full-text search across versions to find when a specific line was added or removed Import/export of the entire version store as a tarball, for offline backup or migration between devices A built-in CodeMirror editor with iRules syntax highlighting, click-to-docs on F5 commands, dark mode, basic linting with opinionated style preferences, and a "test this iRule before saving" pre-flight validation that catches syntax errors before they hit production Everything runs on the BIG-IP itself. No external database, no Git server requirement, no cloud dependency, no agent. The GUI is hosted by the iApps LX worker; the data lives in the extension directory; deploys go through tmsh load sys config merge so any iRule the GUI accepts deploys cleanly. HA awareness is coming next The current release treats each device in an HA pair as an independent island, with its own version history and audit log. The next major release transitions to storing data and metadata in iFiles and/or data-groups, so a unified history follows the rule regardless of which device an edit landed on. A note on iApps LX longevity iApps LX as a framework will be deprecated over time. The replacement is a WASM-based extensibility runtime that we're building toward, and the value of a tool like Rülbased grows in that direction, not shrinks. The job is the same; the substrate becomes faster, sandboxed, and more portable. When the WASM runtime lands, expect Rülbased (or a successor that does the same work) to follow. The Details Everything you need to know is covered in the repo on GitHub. Pop this on a lab box near you, mess around with it, and shoot me feedback either in an issue out there on GitHub or in the comments below. Video Walkthrough164Views0likes0CommentsAPM-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.19KViews7likes67CommentsSwagWAF 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 <[email protected]> #-------------------------------------------------------------------------- # 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...331Views0likes0Comments