SNAT pool persistence
Problem this snippet solves:
This example shows how select the same SNAT address from the SNAT pool for a given client IP address over multiple connections without tracking the selection in memory. The crc32 hash of the client IP address is used to select a SNAT address. See the iRule comments for further details.
Note: If this iRule is utilized on a virtual server that uses OneConnect, set the OneConnect profile's source mask to 255.255.255.255 to ensure the right SNAT pool IP address is used.
To work around an issue where the SNAT pick may be lost (ID374067), apply SNAT for each request. For instance, if using HTTP and OneConnect, you could change CLIENT_ACCEPTED to HTTP_REQUEST. Contact F5 Support references ID374067 for additional information.
Code :
# For v10 and higher, use the static namespace to store the SNAT addresses. Just change "set static::snatpool_name my_snat_pool" in the RULE_INIT event to the name of the SNAT pool for this virtual server. when RULE_INIT { # The only configuration needed is to set the name of the SNAT pool as $static::snatpool_name # Configure the name of the SNAT pool here set static::snatpool_name "my_snat_pool" # Hide the members command from the iRule parser (BZ381099 comment 7) set static::members_cmd "members -list $static::snatpool_name" # Clear any pre-existing array of the same name unset -nocomplain static::snat_ips # Initialize a counter for the number of SNAT pool members set static::i 0 # Loop through the SNAT pool members and add them to an array for faster access # If the SNAT pool is modified, the RULE_INIT code needs to be re-run to re-read the SNAT pool # Make a simple change like adding a space to a comment to force a re-run of RULE_INIT. foreach static::snat_ip [eval $static::members_cmd] { set static::snat_ips($static::i) [lindex $static::snat_ip 0] incr static::i } # Save the number of SNAT IPs to avoid getting the count on every connection set static::array_size [array size static::snat_ips] log local0. "Loaded $static::array_size SNAT IPs from $static::snatpool_name: [array get static::snat_ips]" # Clear the variables we will not use anymore unset static::snatpool_name static::members_cmd static::i static::snat_ip } when HTTP_REQUEST { # Use HTTP_REQUEST instead of CLIENT_ACCEPTED to avoid issue from BZ374067/SOL14098 # Calculate the crc32 checksum of the client IP # Use the modulo of the checksum and the number of SNAT IPs to choose from to select a SNAT IP snat $static::snat_ips([expr {[crc32 [IP::client_addr]] % $static::array_size}]) } # Another variation using the source port of the client to determine the SNAT (Tested on v10, v11) - Bhattman when RULE_INIT { # The only configuration needed is to set the name of the SNAT pool as $static::snatpool_name # Configure the name of the SNAT pool here set static::snatpool_name "my_snat_pool" # Hide the members command from the iRule parser (BZ381099 comment 7) set static::members_cmd "members -list $static::snatpool_name" # Clear any pre-existing array of the same name unset -nocomplain static::snat_ips # Initialize a counter for the number of SNAT pool members set static::i 0 # Loop through the SNAT pool members and add them to an array for faster access # If the SNAT pool is modified, the RULE_INIT code needs to be re-run to re-read the SNAT pool # Make a simple change like adding a space to a comment to force a re-run of RULE_INIT. foreach static::snat_ip [eval $static::members_cmd] { set static::snat_ips($static::i) [lindex $static::snat_ip 0] incr static::i } # Save the number of SNAT IPs to avoid getting the count on every connection set static::array_size [array size static::snat_ips] log local0. "Loaded $static::array_size SNAT IPs from $static::snatpool_name: [array get static::snat_ips]" # Clear the variables we will not use anymore unset static::snatpool_name static::members_cmd static::i static::snat_ip } when HTTP_REQUEST { # Use HTTP_REQUEST instead of CLIENT_ACCEPTED to avoid issue from BZ374067/SOL14098 # Calculate the crc32 checksum of the client IP # Use the modulo of the checksum and the number of SNAT IPs to choose from to select a SNAT IP snat $static::snat_ips([expr {[crc32 [IP::client_addr][TCP::remote_port]] % $static::array_size}]) } # For v9, use a local array to store the SNAT addresses: when CLIENT_ACCEPTED { # Use a local array to configure SNAT addresses. # These addresses need to be defined in a SNAT pool to ensure TMM sends gratuitous ARPs during a failover. # In this example, we use 10 addresses of any subnet. You will probably want to change these to be in the same subnet. # Any number of addresses can be used. set snat_ips(0) 1.1.1.1 set snat_ips(1) 2.2.2.2 set snat_ips(2) 3.3.3.3 set snat_ips(3) 4.4.4.4 set snat_ips(4) 5.5.5.5 set snat_ips(5) 5.5.5.5 set snat_ips(6) 6.6.6.6 set snat_ips(7) 7.7.7.7 set snat_ips(8) 8.8.8.8 set snat_ips(9) 9.9.9.9 # Calculate the crc32 checksum of the client IP # Use the modulo of the checksum and the number of SNAT IPs to choose from to select a SNAT IP snat $snat_ips([expr {[crc32 [IP::client_addr]] % [array size snat_ips]}]) } # For v9, use a local array to store the SNAT addresses using client's source port - Bhattman when CLIENT_ACCEPTED { # Use a local array to configure SNAT addresses. # These addresses need to be defined in a SNAT pool to ensure TMM sends gratuitous ARPs during a failover. # In this example, we use 10 addresses of any subnet. You will probably want to change these to be in the same subnet. # Any number of addresses can be used. set snat_ips(0) 1.1.1.1 set snat_ips(1) 2.2.2.2 set snat_ips(2) 3.3.3.3 set snat_ips(3) 4.4.4.4 set snat_ips(4) 5.5.5.5 set snat_ips(5) 5.5.5.5 set snat_ips(6) 6.6.6.6 set snat_ips(7) 7.7.7.7 set snat_ips(8) 8.8.8.8 set snat_ips(9) 9.9.9.9 # Calculate the crc32 checksum of the client IP # Use the modulo of the checksum and the number of SNAT IPs to choose from to select a SNAT IP snat $snat_ips([expr {[crc32 [IP::client_addr][TCP::remote_port]] % [array size snat_ips]}]) } # Here is a simple test iRule that shows the distribution of the SNAT addresses selected: when RULE_INIT { #========================================================================================= # Logic Test only in RULE_INIT # Use a local array to configure SNAT addresses. # These addresses need to be defined in a SNAT pool to ensure TMM sends gratuitous ARPs during a failover. set snat_ips(0) 1.1.1.1 set snat_ips(1) 2.2.2.2 set snat_ips(2) 3.3.3.3 set snat_ips(3) 4.4.4.4 set snat_ips(4) 5.5.5.5 set snat_ips(5) 5.5.5.5 set snat_ips(6) 6.6.6.6 set snat_ips(7) 7.7.7.7 set snat_ips(8) 8.8.8.8 set snat_ips(9) 9.9.9.9 # Test the distribution of SNAT addresses by tracking the number of hits for each SNAT pool member. # Initialize one variable per SNAT array member for {set j 0} {$j < [array size snat_ips]} {incr j}{ set [set j] 0 } # Loop through a subnet of host addresses for {set i 0} {$i < 256} {incr i}{ # Set a test client IP set ip 10.11.12.$i # Calculate the crc32 checksum of the client IP set hash [crc32 $ip] #log local0. "crc32: $hash" # Use the crc32 hash of the client IP to select a SNAT IP from the array set ip_number [expr {[crc32 $ip] % [array size snat_ips]}] #log local0. "\$ip: $ip, \$snat_ips($ip_number): $snat_ips($ip_number)" # Track which SNAT array member was selected incr $ip_number #log local0. "$ip_number, [set $ip_number]" } log local0. "Results for distribution across the SNAT array members:" for {set j 0} {$j < [array size snat_ips]} {incr j}{ log local0. "$j: [set $j]" } } # Sample log output:: Results for distribution across the SNAT array members: : 0: 27 : 1: 28 : 2: 26 : 3: 29 : 4: 27 : 5: 23 : 6: 20 : 7: 20 : 8: 28 : 9: 28
- EjesNimbostratus
Is there an Idle Timeout value of the SNAT Persist??
I want to know how long it holds.
- Dustin_PlomondoHistoric F5 Account
The incorrect SNAT address issue described in the description of this article is addressed in version 12.0 and 11.5.3-HF2, and more information is available in AskF5 article https://support.f5.com/csp/article/K14098 (ID374067).
- Emmanuel_L_1791Nimbostratus
To mitigate initialization issue a solution could be to use a datagroup instead of Array/snatpool (tested on v11.5.4) :
when RULE_INIT { The only configuration needed is to set the name of the Datagroup(type string) that lists SNAT addresses as $static::snatpool_name_DG Configure the name of the Datagroup set static::snatpool_name_DG "my_snat_pool_DG" Datagroup size set static::snatpool_size [class size "static::snatpool_name_DG"] } when CLIENT_ACCEPTED { Calculate the crc32 checksum of the client IP - Use the modulo of the checksum and the number of SNAT IPs to choose from to select a SNAT IP snat [class element -name [expr {[crc32 [IP::client_addr]] % $static::snatpool_size}] $static::snatpool_name_DG] }
Datagroup exemple (keep the same list as snatpool to be declared as failover object):
ltm data-group internal my_snatpool_DG { records { 10.0.0.1 { } 10.0.0.2 { } 10.0.0.3 { } } type string }
- jmugica_136094Historic F5 Account
"members -list" command is not available on rule init event. This irule fails to load during boot up, and therefore it will probably cause runtime failures during execution
 
https://clouddocs.f5.com/api/irules/members.html:
 
"Note:
 
When assigning a snatpool to static variable and using "members -list" to reference it in RULE_INIT, failures will be observed at startup but won't show up in a reload afterwards. Expected behavior is to fail it in any case as "members -list" is not designed to reference a snatpool name."
 
- JRahmAdmin
I updated that snippet in @wonsoo's comment. Good find, @KroNiX
- ltwagnonRet. Employee
@KroNiX...change has been made. Thanks!
- KroNiXNimbostratus
Hello! You need to change member_cmd to members_cmd at "foreach snat_ip [eval $member_cmd] {"
- wonsoo_41223Historic F5 Account
It looks the iRule execution (RULE_INIT) is earlier than loading SNAT pool, so "members -list $snatpool_name" command execution is failed, and static::snat_ips array not be existed. Try to use this code in HTTP_REQUEST. It checks existence of variable/array and set the data again if the variables are not existed. This setting logic will be run for each tmm process.
when HTTP_REQUEST { if { ![info exists static::array_size] or ![array exists static::snat_ips] } { unset -nocomplain static::snat_ips set snatpool_name "/Common/my_snat_pool" set members_cmd "members -list $snatpool_name" set count 0 foreach snat_ip [eval $members_cmd] { set static::snat_ips($count) [lindex $snat_ip 0] incr count } set static::array_size [array size static::snat_ips] } snat $static::snat_ips([expr {[crc32 [IP::client_addr]] % $static::array_size}]) }
- Sam_O_197776Nimbostratus
Having the same issue that Kai reports above. It appears the RULE_INIT doesn't re-execute on failover and that the static array_size (or the array itself for that matter) are copied between the active and standby.
Just adding or removing a blank line from RULE_INIT and clicking Update re-runs the init and corrects the issue but it's a pain.
- KaiDuerkopNimbostratus
Hey, we're using this irule to persist ~200.000 Client Connections, it works very good.
We have one problem with this iRule. After a BIG-IP Failover the new active BIG-IP has a problem to load the array_size variable.
err tmm4[11616]: 01220001:3: TCL error: /Common/snat_client - can't read "static::array_size": no such variable while executing "expr {[crc32 [IP::client_addr]] % $static::array_size}"