TCL procedures to compress/expand a IPv6 address notation

Problem this snippet solves:

Hi Folks,

the iRule below contains two TCL procedures to convert an IPv6 address from/to human readable IPv6 address notations within iRules.

The

compress_ipv6_addr
procedure shrinks an IPv6 address notation by removing leading zeros of each individual IPv6 address group (as defined in RFC 4291 Section 2.2.1) and by replacing the longest range of consecutive zero value IPv6 address groups with the
::
notation (as defined in RFC 4291 Section 2.2.2). If two or more zero value IPv6 address group ranges have identical lengths, then the most significant IPv6 address groups will be replaced. If the input IPv6 address contains a mixed IPv6 and IPv4 notation (as defined in RFC 4291 Section 2.2.3), the mixed notation will be kept as is.

----------------------------------- compress_ipv6_addr -----------------------------------------

Input: 0000:00:0000:00:0000:0:00:0000            Output: ::                      Time: 16 clicks
Input: 0:00:000:0000:000:00:0:0001               Output: ::1                     Time: 15 clicks
Input: 00:000:0000:affe:affe:0000:000:0%eth0     Output: ::affe:affe:0:0:0%eth0  Time: 20 clicks
Input: 2001:0022:0333:4444:0001:0000:0000:0000%1 Output: 2001:22:333:4444:1::%1  Time: 20 clicks
Input: 2001:1:02:003:0004::0001%2                Output: 2001:1:2:3:4::1%2       Time: 13 clicks
Input: 2001:0123:0:00:000:0000:192.168.1.1%3     Output: 2001:123::192.168.1.1%3 Time: 19 clicks
Input: 0001:0001::192.168.1.1%4                  Output: 1:1::192.168.1.1%4      Time: 11 clicks

----------------------------------- compress_ipv6_addr -----------------------------------------

The

expand_ipv6_addr
procedure expands a compressed IPv6 notation by zero padding each individual IPv6 address group to its full 16 bit representation (as defined in RFC 4291 Section 2.2.1). If the input IPv6 address contains the truncated
::
notation (as defined in RFC 4291 Section 2.2.2), the omitted zero value IPv6 address groups will be restored. If the IPv6 address contains a mixed IPv6 and IPv4 address notation (as defined in RFC 4291 Section 2.2.3), the IPv4 address will be converted into two consecutive IPv6 address groups. If the input contains a malformed IPv6 address which cannot be expanded to a full 128bit IPv6 address, the output will be an empty string.

------------------------------------  expand_ipv6_addr  -----------------------------------------------------

Input: ::                               Output: 0000:0000:0000:0000:0000:0000:0000:0000      Time: 11 clicks
Input: ::1                              Output: 0000:0000:0000:0000:0000:0000:0000:0001      Time: 16 clicks
Input: ::1:2%eth0                       Output: 0000:0000:0000:0000:0000:0000:0001:0002%eth0 Time: 15 clicks
Input: 2001::1%1                        Output: 2001:0000:0000:0000:0000:0000:0000:0001%1    Time: 16 clicks
Input: 2001:1:22:333:4444::%2           Output: 2001:0001:0022:0333:4444:0000:0000:0000%2    Time: 21 clicks
Input: 2001:123::ff:192.168.1.1%3       Output: 2001:0123:0000:0000:0000:00ff:c0a8:0101%3    Time: 29 clicks
Input: 2001:192.168.1.1::10.10.10.10%4  Output: 2001:c0a8:0101:0000:0000:0000:0a0a:0a0a%4    Time: 27 clicks

------------------------------------  expand_ipv6_addr  -----------------------------------------------------

Note: Both procedures are able to handle

%
IPv6 Zone ID suffixes (as defined in RFC 6874) respectively F5's Route Domain notations.

Performance considerations:

Both procedures are performance optimized to maintain a reasonable performance at high execution rates.

The

compress_ipv6_addr
procedure uses two aligned
[string map]
commands to remove any leading zeros without breaking the individual groups, followed by a
[switch]
syntax to detect the longest range of consecutive zero value groups and to execute just a simple
[string range]
command or a combination of the
[substr]
+
[findstr]
commands to perform the final
::
truncation.

The

expand_ipv6_addr
procedure is a little more sophisticated, since it is required to parse the input IPv6 address on a per IPv6 address group basis to zero pad the individual groups, to detect and convert embedded IPv4 addresses and to finally restore the
::
truncation. To reduce the required CPU cycles the
expand_ipv6_addr
procedure makes use of the
$static::ipv6_grp_filler()
,
$static::ipv6_addr_filler()
and
$static::ipv6_dec_map()
array variables (defined during
RULE_INIT
) to allow a very fast lookup of the required IPv6 address group zero paddings, the length of the zero value IPv6 address groups to insert and to translate IPv4 to IPv6 information.

Cheers, Kai

How to use this snippet:

  1. The iRule below contains a
    RULE_INIT
    event which outlines the procedure usage.
  2. Enjoy!

Code :

when RULE_INIT {
    # Initialize the array used to expand compressed IPv6 groups to 16 bit
    array set static::ipv6_grp_filler {
        "1" "000"
        "2" "00"
        "3" "0"
        "4" ""
    }
    # Initialize the array used to expand compressed IPv6 addresses to 128 bit
    array set static::ipv6_addr_filler {
        "0"  "0000:0000:0000:0000:0000:0000:0000:0000"
        "5"  "0000:0000:0000:0000:0000:0000:0000"
        "10" "0000:0000:0000:0000:0000:0000"
        "15" "0000:0000:0000:0000:0000"
        "20" "0000:0000:0000:0000"
        "25" "0000:0000:0000"
        "30" "0000:0000"
        "35" "0000"
        "40" ""
    }
    # Initialize the array used to perform a IPv4 (decimal 0-255) to IPv6 (hex 00-FF) conversation.
    for { set i 0 } { $i <= 255 } { incr i } {
        set static::ipv6_dec_map($i) [format %02x $i]
    }
 
    #
    # Example procedure calls (samples can be removed)
    #
 
    set input "2001:0001:0022:0333:4444:0:0:0:1%1"
    set output [call compress_ipv6_addr $input]
    log local0.debug "Input: $input Output: $output"

    set input "2001:ef:123::192.168.1.1%2"
    set output [call expand_ipv6_addr $input]
    log local0.debug "Input: $input Output: $output"
 
}
proc compress_ipv6_addr { addr } {
    # Enumerate and store IPv6 ZoneID / Route Domain suffix
    if { [set id [getfield $addr "%" 2]] ne "" } then {
        set id "%$id"
        set addr [getfield $addr "%" 1]
    }
    # X encode (e.g. :0001 becomes :X1) leading zeros on the individual IPv6 address groups (left orientated searches)
    set addr [string map [list ":0000" ":X"   ":000" ":X"   ":00" ":X"   ":0" ":X"   "|0000" "X"   "|000" "X"   "|00" "X"   "|0" "X" ] "|$addr|"]
    # Restoring the required X encoded zeros (e.g. :X: becomes :0:) while removing any other X encodings and | separators (right orientated searches)
    set addr [string map [list "X:" "0:"   "X|" "0"   "X." "0."   "X" ""   "|" "" ] $addr]
    # Find the longest range of consecutive zero value IPv6 address groups and then replace the most significant groups with the :: notation.
    switch -glob -- $addr {
        "*::*"            { #Already compressed }
        "0:0:0:0:0:0:0:0" { set addr "::" }
        "0:0:0:0:0:0:0:*" { set addr ":[string range $addr 13 end]" }
        "*:0:0:0:0:0:0:0" { set addr "[string range $addr 0 end-13]:" }
        "0:0:0:0:0:0:*"   { set addr ":[string range $addr 11 end]" }
        "*:0:0:0:0:0:0:*" { set addr "[substr $addr 0 ":"]::[findstr $addr ":0:0:0:0:0:0:" 13]" }
        "*:0:0:0:0:0:0"   { set addr "[string range $addr 0 end-11]:" }
        "0:0:0:0:0:*"     { set addr ":[string range $addr 9 end]" }
        "*:0:0:0:0:0:*"   { set addr "[substr $addr 0 ":0:"]::[findstr $addr ":0:0:0:0:0:" 11]" }
        "*:0:0:0:0:0"     { set addr "[string range $addr 0 end-9]:" }
        "0:0:0:0:*"       { set addr ":[string range $addr 7 end]" }
        "*:0:0:0:0:*"     { set addr "[substr $addr 0 ":0:0:"]::[findstr $addr ":0:0:0:0:" 9]" }
        "*:0:0:0:0"       { set addr "[string range $addr 0 end-7]:" }
        "0:0:0:*"         { set addr ":[string range $addr 5 end]" }
        "*:0:0:0:*"       { set addr "[substr $addr 0 ":0:0:0:"]::[findstr $addr ":0:0:0:" 7]" }
        "*:0:0:0"         { set addr "[string range $addr 0 end-5]:" }
        "0:0:*"           { set addr ":[string range $addr 3 end]" }
        "*:0:0:*"         { set addr "[substr $addr 0 ":0:0:"]::[findstr $addr ":0:0:" 5]" }
        "*:0:0"           { set addr "[string range $addr 0 end-3]:" }
    }
    # Append the previously extracted IPv6 ZoneID / Route Domain suffix and return the compressed IPv6 address
    return "$addr$id"
}
proc expand_ipv6_addr { addr } {
    if { [catch {
        # Enumerating and storing IPv6 ZoneID / Route Domain suffix
        if { [set id [getfield $addr "%" 2]] ne "" } then {
            set id "%$id"
            set addr [getfield $addr "%" 1]
        }
        # Parsing the first IPv6 address block of a possible :: notation by splitting the block into : separated IPv6 address groups
        set blk1 ""
        foreach grp [split [getfield $addr "::" 1] ":"] {
            # Check if current group contains a IPv4 address notation
            if { $grp contains "." } then {
                # The current group contains a IPv4 address notation. Trying to extract the four IPv4 address octets
                scan $grp {%d.%d.%d.%d} oct1 oct2 oct3 oct4
                # Convert the four IPv4 address octets into two IPv6 address groups by querying the $static::ipv6_dec_map array
                append blk1 "$static::ipv6_dec_map($oct1)$static::ipv6_dec_map($oct2) $static::ipv6_dec_map($oct3)$static::ipv6_dec_map($oct4) "
                set oct4 ""
            } else {
                # The current group contains just a IPv6 address notation. Filling up the IPv6 address group with leading zeros by querying the $static::ipv6_grp_filler array
                append blk1 "$static::ipv6_grp_filler([string length $grp])$grp "
            }
        }
        # Parsing the second IPv6 address block of a possible :: notation by splitting the block into : IPv6 address separated groups
        set blk2 ""
        foreach grp [split [getfield $addr "::" 2] ":"] {
            # Check if current group contains a IPv4 address notation
            if { $grp contains "." } then {
                # The current group contains a IPv4 address notation. Trying to extract the four IPv4 address octets
                scan $grp {%d.%d.%d.%d} oct1 oct2 oct3 oct4
                # Convert the four IPv4 address octets into two IPv6 address groups by querying the $static::ipv6_dec_map array
                append blk2 "$static::ipv6_dec_map($oct1)$static::ipv6_dec_map($oct2) $static::ipv6_dec_map($oct3)$static::ipv6_dec_map($oct4) "
                set oct4 ""
            } else {
                # The current group contains just a IPv6 address notation. Filling up the IPv6 address group with leading zeros by querying the $static::ipv6_grp_filler array
                append blk2 "$static::ipv6_grp_filler([string length $grp])$grp "
            }
        }
        # Joining the first and second block of the possible :: notation while expanding the address to 128bit length by querying the $static::ipv6_addr_filler array
        set addr "[join "$blk1$static::ipv6_addr_filler([string length "$blk1$blk2"]) $blk2" ":"]"
    }] } then {
        # log local0.debug "errorInfo: [subst \$::errorInfo]"
        # return "errorInfo: [subst \$::errorInfo]"
        return ""
    }
    # Append the previously extracted IPv6 ZoneID / Route Domain suffix and return the expanded IPv6 address notation
    return "$addr$id"
}

Tested this on version:

12.0
Updated Jun 06, 2023
Version 2.0
  • Hi Kai,

     

    good job.

     

    for the compress procedure, I suggest to use this code which is more performant (around 5 clicks):

     

    proc compress_ipv6_addr { addr } {
         Enumerate and store IPv6 ZoneID / Route Domain suffix
        if { [set id [getfield $addr "%" 2]] ne "" } then {
            set id "%$id"
            set addr [getfield $addr "%" 1]
        }
    
        return "[IP::addr $addr mask ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]$id"
    }
    

    and the following procedure (50% clicks more) to prevent the shared variables requirements

     

    proc expand_ipv6_addr_sp { addr } {
        if { [catch {
             Enumerating and storing IPv6 ZoneID / Route Domain suffix
            if { [set id [getfield $addr "%" 2]] ne "" } then {
                set id "%$id"
                set addr [getfield $addr "%" 1]
            }
             Parsing the first IPv6 address block of a possible :: notation by splitting the block into : separated IPv6 address groups
            set blk1 ""
            foreach val [split [getfield $addr "::" 1] ":"] {
                if { $val contains "." } then {
                         The current group contains a IPv4 address notation. Trying to extract the four IPv4 address octets
                        scan $val {%d.%d.%d.%d} oct1 oct2 oct3 oct4
                         Convert the four IPv4 address octets into two IPv6 address groups
                        append blk1 [format "%02x%02x %02x%02x " $oct1 $oct2 $oct3 $oct4]
                        unset -nocomplain oct1 oct2 oct3 oct4
                    } else {
                        append blk1 "[format %04x 0x$val] "
                    }
            }                           
            set blk2 ""
            foreach val [split [getfield $addr "::" 2] ":"] {
                if { $val contains "." } then {
                         The current group contains a IPv4 address notation. Trying to extract the four IPv4 address octets
                        scan $val {%d.%d.%d.%d} oct1 oct2 oct3 oct4
                         Convert the four IPv4 address octets into two IPv6 address groups
                        append blk2 [format "%02x%02x %02x%02x " $oct1 $oct2 $oct3 $oct4]
                        unset -nocomplain oct1 oct2 oct3 oct4
                    } else {
                        append blk2 "[format %04x 0x$val] "
                    }
            }
            set addr "[join "$blk1[string repeat "0000 " [expr {8 - [string length "$blk1$blk2"]/5}]] $blk2" ":"]"
        }] } then {
             log local0.debug "errorInfo: [subst \$::errorInfo]"
             return "errorInfo: [subst \$::errorInfo]"
            return ""
        }
         Append the previously extracted IPv6 ZoneID / Route Domain suffix and return the expanded IPv6 address notation
        return "$addr$id"
    }
    
  • Hi Stanislas,

    cool stuff, didn't know that the build-in

    [IP::addr]
    command can handle mixed IPv4/IPv6 notations and will automatically compress IPv6 notations for you. The only difference in the output of my and your iRule is, that your compress procedure (aka. make an IPv6 more human readable) will also remove the mixed IPv4/IPv6 notation. Well milage may vary which notation is more human readable...

    And the second iRule and its

    [string repeat]
    approach is 1:1 comparable with one of the development iRules which I had before I optimized the code...

    Cheers, Kai