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
Published Mar 18, 2015
Version 1.0
  • 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}"
    
  • 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.

     

  • wonsoo_41223's avatar
    wonsoo_41223
    Historic 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}])
    }
    
  • KroNiX's avatar
    KroNiX
    Icon for Nimbostratus rankNimbostratus

    Hello! You need to change member_cmd to members_cmd at "foreach snat_ip [eval $member_cmd] {"

     

  • I updated that snippet in @wonsoo's comment. Good find, @KroNiX

     

  • jmugica_136094's avatar
    jmugica_136094
    Historic 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."

     

  • 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
    }
    
  • Ejes's avatar
    Ejes
    Icon for Nimbostratus rankNimbostratus

    Is there an Idle Timeout value of the SNAT Persist??

    I want to know how long it holds.