iRules
714 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.15KViews7likes62CommentsThe table Command: Examples
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this last part of the series on the usage of the new table command, we discuss various examples of the use of the table command. Examples Now that we've covered all of the pieces of the table command, let's see some larger real-world uses of making them work together, not only simplfying existing iRules, but making new things possible. Limiting Connections To A VIP In the counting article, we gave one powerful example iRule that would have been too bulky and/or unreliable to do before: limit a VIP to a certain number of connections. That example can be easily modified to limit those connections by IP address as well. If you compare it to this example when CLIENT_ACCEPTED { set tbl "connlimit:[IP::client_addr]" set key "[TCP::client_port]" if { [table keys -subtable $tbl -count] > 1000 } { event CLIENT_CLOSED disable reject } else { table set -subtable $tbl $key "ignored" 180 set timer [after 60000 -periodic { table lookup -subtable $tbl $key }] } } when CLIENT_CLOSED { after cancel $timer table delete -subtable $tbl $key } you can see that only the first two lines changed! We've gone from one global subtable to a subtable for each IP address. This is an example of the power of subtables and using them for scoping. Blocking DNS Flood Attacks This example to blacklist IPs if they make too many DNS queries per second, uses global arrays to keep track of the number of requests made in the current second, as well as the blacklist of IPs. when RULE_INIT { set ::maxquery 100 set ::holdtime 600 } when CLIENT_DATA { set srcip [IP::remote_addr] set curtime [clock second] if { [ info exists ::blacklist($srcip) ] } { if { $::holdtime > [expr ${curtime} - $::blacklist($srcip) ] } { drop return } unset ::blacklist($srcip) } if { [ info exists ::usertable(time,$srcip)] and $curtime == $::usertable(time,$srcip) } { incr ::usertable(freq,$srcip) if { $::usertable(freq,$srcip) > $::maxquery } { set ::blacklist($srcip) $curtime unset ::usertable(freq,$srcip) unset ::usertable(time,$srcip) drop return } } else { set ::usertable(freq,$srcip) 1 set ::usertable(time,$srcip) $curtime } } It has to do all of the work to remove old data from those arrays manually, so not only can that cause all those array entries to just accumulate in memory, but it demotes the VIP it gets attached to (because it uses global variables) . If we use the table command instead as shown in the following code, a lot of the complexity is handled by the system, so it's now cleaner, simpler, and CMP-compatible. A win all around! when RULE_INIT { set ::maxquery 100 set ::holdtime 600 } when CLIENT_DATA { set srcip [IP::remote_addr] set curtime [clock second] if { [ info exists ::blacklist($srcip) ] } { if { $::holdtime > [expr ${curtime} - $::blacklist($srcip) ] } { drop return } unset ::blacklist($srcip) } if { [ info exists ::usertable(time,$srcip)] and $curtime == $::usertable(time,$srcip) } { incr ::usertable(freq,$srcip) if { $::usertable(freq,$srcip) > $::maxquery } { set ::blacklist($srcip) $curtime unset ::usertable(freq,$srcip) unset ::usertable(time,$srcip) drop return } } else { set ::usertable(freq,$srcip) 1 set ::usertable(time,$srcip) $curtime } } Limiting POST Requests Per User Another example to limit POSTs per-user uses even more very tricky global array manipulation. when RULE_INIT { set ::windowSecs 10 } when HTTP_REQUEST { if { [HTTP::method] eq "POST" } { if { ![HTTP::header exists Authorization] } { HTTP::respond 401 return } set myUserID [getfield [b64decode [substr [HTTP::header "Authorization"] 6 end]] ":" 1] set myMaxRate [findclass $myUserID $::MaxPOSTRates3 " "] if { $myMaxRate ne "" }{ set currentTime [clock seconds] if { [info exists ::timestamp($myUserID)] } { set i [ expr $currentTime - $::timestamp($myUserID) ] if { $i > $::windowSecs } { set i $::windowSecs } while { $i > 0} { for {set j $::windowSecs} {$j > 0} {set j [expr $j - 1]} { set k [ expr $j - 1 ] set ::count($myUserID.$j) $::count($myUserID.$k) } set ::count($myUserID.0) 0 incr i -1 } set k 0 for {set i 0} { $i < $::windowSecs} {incr i} { incr k $::count($myUserID.$i) } if {$k < $myMaxRate } { incr ::count($myUserID.0) set ::timestamp($myUserID) $currentTime } else { HTTP::respond 302 Location http://asdf:44444/ } } else { for {set i 0} {$i < $::windowSecs} {incr i} { set ::count($myUserID.$i) 0 } set ::timestamp($myUserID) $currentTime set ::count($myUserID.0) 1 } } } } It can be very difficult to understand, to say nothing of trying to maintain or alter it. With the table command to do all of the heavy lifting for us, it becomes much simpler, lighter-weight, and CMP-compatible. when RULE_INIT { set static::windowSecs 10 } when HTTP_REQUEST { if { [HTTP::method] eq "POST" } { if { ! [HTTP::header exists Authorization] } { HTTP::respond 401 return } set myUserID [getfield [b64decode [substr [HTTP::header "Authorization"] 6 end]] ":" 1] set myMaxRate [findclass $myUserID $::MaxPOSTRates3 " "] if { $myMaxRate ne "" } { set reqnum [table incr "req:$myUserId"] set tbl "countpost:$myUserId" table set -subtable $tbl $reqnum "ignored" indef $static::windowSecs if { [table keys -subtable $tbl -count] > $myMaxRate } { HTTP::respond 302 Location http://asdf:44444/ return } } } } That's pretty impressive, if I do say so myself. And once again, because the table command will expire the entries automatically, there's no need to worry about old data taking up memory. We cannot wait to see all of the new (or old!) problems you solve with the help of the table command. Don't forget to show off your work in the iRules CodeShare!3.2KViews0likes4CommentsThe table Command: The Fine Print
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this eighth part of the series on the usage of the new table command, we discuss the limitations, a few advanced details and plans for the future. Limitations As fantastic as the new session table is, it does have its limitations. You can't use the table command in RULE_INIT or any other global event. There's no way to access the session table outside of tmm. The ability to count and list keys is limited to subtables. Timestamps are limited to 1-second resolution, so there's no way to do counts for sub-second events. As you saw, you can use it for accurate counting, but that's not nearly as easy to use as we'd like it to be. We tried very hard to come up with a command, or set of commands, that would hide all the table manipulation and just do the counting for you, but everything we came up with was either very complicated or would always use the most computationally expensive method for counting, and that seemed like a bad tradeoff. Advanced details Because there is only one global session table, the session command and the table command both act on the same data. For example, if you insert an entry into the session table with the session command, you could then later look up that data (perhaps in another iRule) using the table command. However, the session command cannot access any data in a subtable in any way. The session table is mirrored to the standby, and this cannot be disabled. While most of the table commands are fairly low weight, getting the list of keys in a subtable is very heavy weight. Because of the new features that subtables have, you might be tempted to use one giant subtable for all your data. This is probably a bad idea, because of the way that the session table is arranged across all the CPUs in a CMP system. Entries in the session table are distributed across all processors. Subtables themselves are also distributed across all processors. However, all of the entries in a given subtable are on the same processor. So if you put all of your entries (or the vast majority of them) into the same subtable, then one CPU will take a disproportionate amount of memory and load. Which you probably don't want. So, in general, more, smaller subtables are a better arrangement than fewer, larger subtables. Plans for the future We think that we've provided a great set of features here, and that they will help you a great deal, but we recognize that it's probably not perfect. Also, we have lots of other plans for future enhancements to the session table and the table command. Ideally, this would completely take the place of global variables in iRules. We would love to get your feedback on what is most important to fix or add. The best way to do that is not to merely suggest features, but to supply us with actual use cases. What can't you do with the existing commands? What can you do, but only in an ugly way?1.2KViews0likes4CommentsThe table Command: Counting
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this seventh part of the series on the usage of the new table command, we discuss counting techniques. Counting It turns out that one popular use of iRules is to count things and then report or act on those counts. It also turns out that there are a lot of different ways of counting things, and they require slightly different techniques. The four major ways of counting that we've found so far are (in order of increasing complexity): Events that happened ever (or recently) Events that happened in the past 1 second (or other fixed window) Events that happened in the past N seconds (or other sliding window) Events that are happening right now We'll cover each of these in turn. Events that happened ever How many requests have been made on this VIP? How many POSTs has user X made? This is easy: just use table incr and table lookup. Most of the time you’ll only care about things that happened recently, so you can specify a different timeout for the entry, or just use the default. If you truly want to keep track of things ever, then you can also specify an indefinite timeout. This does mean that that memory will never go away on its own, though, so you should be careful with that option. :) Events that happened in the last fixed window How many DB queries were made in the last second? How many requests has IP X made in the last 10-second window? This is also pretty easy. Each window can have a name (for example, each 1 second window can be named by the result of [clock second]), and you just need to somehow combine that name with the name of the event you're counting. For example: table incr "dbquery:[clock second]" set tensecwin "[string range [clock second] 0 end-1]" table incr "ipreq:[IP::client_addr]:$tensecwin" Events that happened in the last sliding window How many hits on the login page in the last hour? How many bytes sent to IP X in the last minute? How many connections from user Y in the last 15 seconds? What users logged in in the last day? While a fixed window is useful, more often a sliding window is what is desired. This is because a fixed window doesn't move with the clock. For example, if you're using a fixed 60-second window, and it's now 12:30:14, you can't know how many things happened "in the last 60 seconds". You only can know how many things happened from 12:30:00-now, and maybe from 12:29:00-12:29:59, if you kept the result from the previous window. If you had a sliding window instead, it would always contain "the last 60 seconds", meaning that if it's 12:30:14 then the window is from 12:29:15-12:30:14, and when it's 12:30:17, then the window is from 12:29:18-12:30:17. For something like rate-limiting, this makes a huge difference in user experience. Unfortunately, the things that make it more desirable are the very things that make it more complicated. I'll explain this by considering each window as a bucket. With a fixed window, you only ever add things to the bucket ("user X made a request that gets counted in bucket #12:29"). With a sliding window, sometimes things need to come out of the bucket ("the bucket is now counting from 12:29:18 to 12:30:17, so the request that was made at 12:29:17 now needs to be removed"). This tells us that getting a sliding window means managing specific items (so we need to have a handle or name for each item) and expiring them as they move out of the window. Fortunately, a subtable is exactly up for the job. To implement a sliding window, you would do something like: set reqno [table incr "reqs:$username"] table set -subtable "reqrate:$username" $reqno "ignored" indefinite 60 log local0. "User $username made [table keys -count -subtable "reqrate:$username"] requests in the past 60 seconds" This keeps track of the rate of requests for each user. The first line gives each request a serial number (it just increments a variable), and the second line adds an entry to that user's subtable. The entry will expire on its own when it is supposed to leave the window, and counting the keys in the subtable will tell us the number of requests that user made in the window. The more observant among you will note that the table entry we're using for the serial number is going to expire too, so if this user is idle for a while, then the next request will have the serial number go back to 0. This is perfectly OK, though, since as long as the window size (in this example, it’s 60 seconds) is less than the timeout of the serial number entry (in this example, it’s the default of 180 seconds), then by the time the serial number entry expires, all of the entries in the subtable will have expired also. So it doesn't matter that the serial numbers starts at 0 again. If you want to use a longer window than 180 seconds, then you'll need to change the timeout to be longer after the incr line. Now, there's no rule that says you have to have a separate variable to keep track of the identity of each thing you're counting, or that they have to be consecutive. If you already have a serial number (for example, if you're counting new JSESSIONIDs), then you can just use that. Events that are happening right now How many client connections to this VIP? What users are currently logged in? This is slightly tricky, but doable. It builds on the concepts last example, with some additions. One simple example is: when HTTP_REQUEST { switch [HTTP::uri] { "/login.html" { table set -subtable userlist $username "ignored" $static::useridletimeout } "/logout.html" { table delete -subtable userlist $username } default { table lookup -subtable userlist $username } } log local0. "[table keys –count –subtable userlist] users currently logged in" } Once again, we're adding an entry to a subtable to start counting something, but in this case not only are we removing the entry when we want to stop counting it, but we also do a lookup on that entry when the user accesses any page. The reason we do that is to keep the entry alive as long as the user is busy, so if the user doesn't log out via our logout page, we still stop counting that user (because that user's entry in our subtable will expire) when our application does (after the number of seconds in $static::useridletimeout) . With a similar method, we can do something like limit a VIP to 1000 connections: when CLIENT_ACCEPTED { set tbl "connlimit" set key "[IP::client_addr]:[TCP::client_port]" if { [table keys -subtable $tbl -count] > 1000 } { event CLIENT_CLOSED disable reject } else { table set -subtable $tbl $key "ignored" 180 set timer [after 60000 -periodic { table lookup -subtable $tbl $key }] } } when CLIENT_CLOSED { after cancel $timer table delete -subtable $tbl $key } The basics are the same as the previous examples, but now we've added a timer to keep the table entry alive, rather than relying on any particular iRule event. This works because when the connection closes we kill the timer and delete the entry. Now, I know what you're asking: "Why all the extra complexity? Heck, if I had table decr, I could just do something like:" when CLIENT_ACCEPTED { if { [table lookup "conns"] > 1000 } { event CLIENT_CLOSED disable reject } else { table incr "conns" } } when CLIENT_CLOSED { table decr "conns" } The answer is: reliability. What happens if CLIENT_CLOSED never fires? Maybe your box runs low on memory and the connection sweeper hits. Maybe you're running on a VIPRION with no HA and a blade goes down. Your user count never gets to be decremented, and so now you start rejecting connections because you think your VIP is busier than it is. And that's why there is no table decr: to discourage exactly this sort of code. If you think you need it, you probably shouldn’t be using increment and decrement to solve your problem. In the real example above, if CLIENT_CLOSED never fires, there's no problem. No matter how the connection dies, the timer will be removed, so it will stop touching the entry, so the entry will expire on its own. In this way, your connection count may only be wrong temporarily; it will settle on the right answer all on its own.2KViews0likes0CommentsThe table Command: Subtables
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this sixth part of the series on the usage of the new table command, we discuss how you can create and manage subtables within the session table. Subtables Whew! Still with me? Good, because we're just now getting to some of best stuff. One of the most powerful changes to the session table in v10.1 is the concept of a subtable, which is simply a named set of entries. It lets you organize your entries in groups, and lets you act on those groups. A subtable is basically a table in and of itself, with some special abilities. Subtables only contain entries, though; you can’t nest subtables. Here's what they look like: They are trivial to use: every current table subcommand simply takes a -subtable <name> parameter. So you can do things like: table set -subtable $tbl $key $data table lookup -subtable $othertbl $key table delete -subtable $thirdtbl $key That's all there is to it! Doesn't look like much, does it? But this is very powerful stuff! Maybe you want a different set of data for each user, or for each client IP address, or for each virtual server? No problem! table set -subtable "$username" $key $data table add -subtable "[IP::client_addr]" $key $data table replace -subtable "[virtual]" $key $data Since you can name your subtables however you want, you can now have shared variables with any arbitrary scope! And there's an easy way to clean up: table delete -all -subtable "$username" will remove every entry in a subtable in one fell swoop. But wait! There's more! With subtables comes a new and long-desired ability: the ability to list or count the number of keys: table keys -subtable <name> [-count|-notouch] You’ll note that unlike all the other table subcommands, the subtable name is not optional here: there’s no way to use it on entries that aren’t in a subtable. Also note that by default, retrieving the list of keys will touch each one, but getting the count will never touch any entries. Isn't this AWESOME!?! This feature is going to simplify a lot of your iRules, and give you the ability to do things that simply weren't feasible before. I can't wait to see all the creative uses you come up with! That covers all of the new session table features. Now we'll cover some ways of putting everything together...3.2KViews0likes5CommentsThe table Command: Advanced Data Expiration
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this fifth part of the series on the usage of the new table command, we discuss advanced concepts around data lifetime and expiration from the session table. Advanced Data Expiration On the off chance that your iRule does need to keep close track of varying timeout or lifetime values, then you should know some important ways that they might or might not be changed. As you might expect, all of the other subcommands to set values take the timeout and lifetime parameters as well: table set [-excl|-mustexist] <key> <value> [<timeout> [<lifetime>]] table add <key> <value> [<timeout> [<lifetime>]] table replace <key> <value> [<timeout> [<lifetime>]] However, the add and replace subcommands are designed to sometimes not update the data. For example, if you call table add with a key that already exists, then the data will not get updated. In all cases, if the table command doesn't update the data, then it doesn't update the timeout and lifetime either. If it does update the data, then it does update the timeout and lifetime. So, if you have a table that looks like: and you run: table set 10.1.2.3 UserQuux indefinite 3600 then you end up with: The timeout and lifetime both got updated because the data was updated. If you then ran: table add 10.1.2.3 UserBar 120 120 then the table would not change at all. Because the data did not get updated, the timeout and lifetime also didn't. Sometimes you might want to update the value and not touch the existing timeout or lifetime values, even though you don't know what they are. By specifying a 0 for either the timeout or the lifetime, or by omitting the lifetime, that parameter is not changed even if the value is updated. So if you had the previous table and then ran: table replace 10.2.9.12 UserZap 180 0 you would get: You can see that the entry's value and the timeout were changed, but the lifetime was not.1.2KViews0likes0CommentsThe table Command: Data Expiration
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this fourth part of the series on the usage of the new table command, we discuss data lifetime and expiration from the session table. Data expiration Remember that I mentioned that an entry also has metadata? Prior to v10.1, the only metadata that an entry had was a timestamp, which keeps track of when that entry was last touched (changed or queried), and a timeout. The touch timestamp can’t be directly set by iRules, of course. It’s used internally by the session table to keep track of when to expire entries. So a small session table might have looked like this: You can see a number of different types of keys and values, each one with a timeout and a timestamp. One common request to enhance the session table has been to have some way of looking up an entry without affecting the touch timestamp, so we added the -notouch flag to the table command: table set [-notouch] $key $data table lookup [-notouch] $key With this flag, you can query the session table without changing when the entries will expire. But we felt that that didn't go far enough for some uses, so while the session table looks much the same in v10.1, it has two additions: Just as before, an entry has a touch timestamp and can have a timeout, but now it also has a create timestamp (to record when the entry was created), and it can also have a lifetime. By setting a lifetime on an entry, you can have it expire after a certain period of time no matter how many changes or lookups are performed on it. An entry can have a lifetime and a timeout at the same time. It will expire (be removed from the table) whenever the timeout OR the lifetime expires, whichever comes first. You can just specify the lifetime right after the timeout value: table set <key> <value> [<timeout> [<lifetime>]] If you want an entry to only have a lifetime, and not a timeout, you can specify an indefinite timeout. For example: table set $key $data indefinite 3600 will set an entry that will be removed after 3600 seconds. By default, an entry has a timeout of 180 seconds (just like previous versions), and an indefinite lifetime. For most uses of the session table, the user will specify timeout and lifetime values when an entry gets set, and never explicitly worry about them afterwards. If you do need to query or change those values on an entry, then you can do so directly: table timeout [-remaining] $key table timeout $key <value> table lifetime [-remaining] $key table lifetime $key <value> These commands operate exactly as they read: you can query or set the lifetime or timeout parameter which the entry has, or you can query the remaining time that an entry has left. Note that these commands never affect the expiration of a record. In other words, they always implicitly have the -notouch flag set.1.9KViews0likes0CommentsThe table Command: New Ways To Alter Data
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this third part of the series on the usage of the new table command, we discuss ways to alter data in the session table. New Ways To Alter Data Of course, that's not the only way that two iRules running at the same time could stomp on each others changes. Very often you'll want to use iRules to do something like count how many requests a user makes. Without the table command, you might try to do something like: set reqs [session lookup uie $user] if { $reqs == {} } { set reqs 0 } incr reqs session add uie $user $reqs But this runs into the same problem: two different connections could lookup the value at the same time, each one will incremement it once, and each one will then write back the same value. So even though two connections came in, the value only increases by one. Not good. With the table command, there are two ways to alter values in the session table: table incr [-mustexist] $key [$value] table append [-mustexist] $key $value table incr will add the specified value (or 1 if you don't specify a value) to the existing entry (or 1 if you don't specify a value), and table append will append the specified value. Both commands will return the value after it was changed. What happens if there isn't an entry with the specified key? By default, an entry is created with an empty value (for incr, that would be 0, and for append it would be the empty string), which is then incremented or appended to as appropriate. If you don't want an entry created by default, you can specify the -mustexist flag, which will make the command do nothing and return the empty string if there’s no existing entry. Because these are atomic operations (meaning that they can't be interrupted by another iRule running another table command), that means the above example become just one line: set reqs [table incr $user] How spiffy is that? There are two important caveats with table incr: first, don't try to increment a value that isn't a number. For example, if you have a key with an associated value of "foo", trying to increment that will cause a Tcl error ("Illegal value"). Second, you'll note that there is no table decr command. Why? If you want decr, then that means that you're counting things that go away (e.g. counting the number of users that are logged in). What happens if the connection is killed and you never get to run table decr? Your count will be wrong, and there won't be any way to fix it. There are ways of keeping an accurate count of things like that, which we'll cover in another section. If you're counting things that can't go away (e.g. a user can't "unmake" an HTTP request), then table incr is fine.984Views0likes1CommentThe table Command: New Ways To Set Data
With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command... In this second part of the series on the usage of the new table command, we discuss ways to assign data to the session table. New Ways To Set Data The session add command will always set a value associated with a key, overwriting anything that was there before it. If you are keeping track of the last URI that a user visited, for example, then this isn't a problem: the new URI value in the table overwrites the old one. But if you were trying to roll your own type of persistence, then this behavior is less good. (In reality, you should use the persist command for this sort of thing, as it's designed to handle situations like this. I'm just using persistence as a concrete example.) You might try to write an iRule that looks something like: set newpmbr [session lookup uie $key] if { $newpmbr == {} } { session add uie $key $curpmbr } else { pool [LB::server pool] member $newpmbr } Makes sense: use an existing entry if there is one, or add one if not. But what happens if two connections come in at very close to the same time? The first one will do a lookup, see that there's no entry in the table, and start to add an entry. But since there are now multiple processors working at the same time, the second connection could do its lookup after the first connection did its lookup, but before the first connection added an entry. If that happens, both connections will act as if they are the first, and they could go to different servers! Oops! To address problems like that, we've added two other ways to add data to the session table: table set $key $data table add $key $data table replace $key $data table set acts just like session add does: if the key doesn't already exist in the table, it adds the entry, and if it does exist, it overwrites what's there. table add won't overwrite an existing entry, it will only add an entry if the key doesn't already exist. table replace is the opposite: it will only update an existing entry. If the key doesn't already exist in the session table, it does not add a new entry. If you've seen memcache, then this syntax will seem very familiar to you. But if you haven't, and you don't want to have three different commands in your iRules that all set data, you can instead supply flags to the table set command that provide the exact same behavior. table set -excl is equivalent to table add. table set -mustexist is equivalent to table replace. Some users might prefer one way over the other, and using the flags might be more obvious to a newcomer over the more subtle set/add/replace distinction. But they really are just synonyms. Also, even though it's not shown right here, all of these commands take an optional timeout, as well as many other parameters. The examples of table command syntax in this series won't give a complete command set, so we can focus on just one feature at a time, and won't strictly follow the usual syntax conventions. A complete syntax reference can be found in the iRules documentation. With these commands, we can now rewrite the above example in a way that will actually work: set newpmbr [table add $key $curpmbr] if { $newpmbr != $curpmbr } { pool [LB::server pool] member $newpmbr } (Note that this isn’t a complete example of persistence, because things like timeout handling and persistence for different pools aren’t covered.) Because table add will never overwrite an existing entry, it doesn't matter how many connections happen at the same time. The first one will always win and write its pool member to the session table entry, and all the other connections will get that pool member as a result of the table add command. Note that no matter if the table add command wrote the entry or not, it returns the value of the entry, so all connections accessing this session table entry will get the same result, no matter which one was first.1.6KViews0likes2CommentsThe table Command: Basics
The BIG-IP is a powerful tool when it comes to managing your application traffic. One of the reasons is F5's CMP technology: the ability to scale across many CPUs by running independently on each of them. As any advanced iRule writer could tell you, though, this independence can sometimes make it difficult or impossible to treat the system as one cohesive whole. The problem is that maintaining a consistent set of data spread across processors like that is a long-standing one in computer science. There's just no way to do it in a general enough way for something like Tcl global variables to work and still get good performance. In v10, the session table was the one small escape hatch for any iRule to have a globally available set of shared data and not be pinned to a single processor. Although it has existed in iRules for some time, the session command was never designed to be a general purpose tool for this sort of thing: the interface has a lot of unnecessary complexity, and there is significant missing functionality. But when you get down to brass tacks, it works; it solves the problem and maintains the speed we need.With version 10.1, we've given the session table some long-sought functionality, and revamped its iRules interface completely to give users a new, cleaner, full-featured way to keep track of global data. We are very proud to introduce the table command...There's quite a lot to cover, so we'll take it slowly. The Basics At its heart, the table command is just another way to access the session table, and the fundamental concepts are not much different than they were before. The session table is a global table shared across all processing units in the BIG-IP system. It stores values that are indexed by keys. An entry in the session table consists of a key, a value, and some associated metadata (which will be covered later). The session command allows you to set a key/value pair, look up a value for a given key, or delete a key/value pair. It looks like this: session add <mode> <key> <data> [<timeout>] session lookup <mode> <key> session delete <mode> <key> where <mode> is a value that no longer has any meaning in v10, and <key> can take some baroque syntax, which is also meaningless in v10. When those commands are used, it might look like: session add uie "$key any pool any virtual" $data 180 session lookup uie "$key any pool" session delete uie "$key" To someone unfamiliar with iRules, this might not make a lot of sense. What's "uie"? Why the "any pool" business? And what does any of this have to do with sessions?When we set out to improve this interface, the first thing we did was to come up with a new command name, one that might hint that this is something general purpose that we're dealing with: hence, the table command. A new command name also ensures that any existing iRules using the session command wouldn't be interfered with. The second thing we did was get rid of all those bulky unused parameters. The table command’s basic form looks like: table set $key $data 180 table lookup $key table delete $key That's already an improvement! But we didn't stop there. Not by a long shot.3KViews0likes2Comments