Structured IPv6 Address

The following script takes an ip6, ip6-prefix or str and parses it into a structure (key-value array) that gives direct access to various IPv6 components.

# Usage:
#
# - $parseIP6Address ip6 [prefix length] [detail=yes]
# - $parseIP6Address ip6-prefix [detail=yes]
#
# Returns:
#
# - address (ip6): IPv6 address of the input
#   addressPrefix (ip6-prefix): IPv6 address-prefix of the input
#   prefix (ip6): Prefix of the input
#   prefixLength (num): Prefix length of the input
#
# If invoked with detail=yes, then the "detail" key will also be set to one of:
#
# - type (str): "unspecified"
#   fields (array): Address as an array of 8 integers
#
# - type (str): "loopback"
#   fields (array): Address as an array of 8 integers
#
# - type (str): "link-local"
#   fields (array): Address as an array of 8 integers
#
# - type (str): "ip4-mapped"
#   fields (array): Address as an array of 8 integers
#   ip4 (ip): Embedded IPv4 address
#
# - type (str): "ip4-compatible"
#   fields (array): Address as an array of 8 integers
#   ip4 (ip): Embedded IPv4 address
#
# - type (str): "multicast"
#   fields (array): Address as an array of 8 integers
#   flags (num): Multicast flags
#   groupID (num): Multicast group ID
#
# - type (str): "multicast"
#   fields (array): Address as an array of 8 integers
#   flags (num): Multicast flags
#   subnetPrefix (ip6): Unicast base
#   subnetPrefixLength (num): Length of the unicast base
#   groupID (num): Multicast group ID
#   [RIID] (ip): address of the embedded multicast rendezvous point; optional
#   SSM (bool): Whether multicast address is source-specific
#
# - type (str): "unicast"
#   subtype (str): "ula" if Unique Local IPv6 Unicast; otherwise "gua"
#   fields (array): Address as an array of 8 integers
#   globalID (ip6): Site identifier
#   subnetID (ip6): Subnet identifier within site
#   interfaceID (ip6): Interface identifier on a link
#

# Return IPv6 prefix mask of given length.
#
# $1 (integer): prefix length
#
# > :put [$MakeIP6PrefixMask 64]
# ffff:ffff:ffff:ffff::
:global MakeIP6PrefixMask do={
    :if (($1 < 0) or ($1 > 128)) do={ :error "$1 is invalid IPv6 mask length" }

    :local varMask ""

    :if ($1 >= 16) do={
        :set varMask "ffff"
        :for i from=1 to=(($1 / 16) - 1) step=1 do={
            :set varMask ($varMask . ":ffff")
        }
    }

    :local varRemBits ($1 % 16)

    :if ($varRemBits > 0) do={
        :if ([:len $varMask]) do={
            :set varMask ($varMask . ":")
        }

        :for i from=1 to=4 do={
            :if ($varRemBits >= 4) do={
                :set varMask ($varMask . "f")
                :set varRemBits ($varRemBits - 4)
            } else={
                :set varMask ($varMask . ({0;1;3;7}->($varRemBits % 4)))
                :set varRemBits 0
            }
        }
    }

    :if ($1 <= 112) do={
        :set varMask ($varMask . "::")
    }

    :return [:toip6 $varMask]
}

# Return IPv6 suffix mask of given length.
#
# $1 (integer): prefix length
#
# > :put [$MakeIP6PrefixMask 64]
# ::ffff:ffff:ffff:ffff
:global MakeIP6SuffixMask do={
    :global MakeIP6PrefixMask

    :if (($1 < 0) or ($1 > 128)) do={ :error "$1 is invalid IPv6 mask length" }
    :return (~[$MakeIP6PrefixMask (128 - $1)])
}

# Return given IPv6 address as an array of eight 16bit integers,
# one integer per field.
#
# $1 (ip6, str): IPv6 address
#
# > :put [$MakeIP6AddressFields 2001:db8::1]
# 8193;3512;0;0;0;0;0;1
:global MakeIP6AddressFields do={
    :local varAddr [:tostr $1]
    :local varAddrLen [:len $varAddr]

    # ::
    :if ($varAddrLen = 2) do={ :return {0 ; 0 ; 0 ; 0 ; 0 ; 0 ; 0 ; 0} }

    :local funcParseFields do={
        :local varFieldParts ({})
        :local varFieldIdx 0
        :local varDelimIdx [:find $1 ":" $varFieldIdx]

        :while ($varDelimIdx) do={
            :set varFieldParts ($varFieldParts , [:tonum "0x$[:pick $1 $varFieldIdx $varDelimIdx]"])
            :set varFieldIdx ($varDelimIdx + 1)
            :set varDelimIdx [:find $1 ":" $varFieldIdx]
        }

        :set varFieldParts ($varFieldParts , [:tonum "0x$[:pick $1 $varFieldIdx [:len $1]]"])
        :return $varFieldParts
    }

    :local varDelimIdx [:find $varAddr ":"]

    # ::x
    :if ($varDelimIdx = 0) do={
        :local varFieldsTail [$funcParseFields [:pick $varAddr 2 $varAddrLen]]
        :local varFieldsHead ({})
        :for i from=0 to=(7 - [:len varFieldsTail]) do={ :set varFieldsHead ($varFieldsHead , 0) }
        :return ($varFieldsHead , $varFieldsTail)
    }

    # x::
    :if ($varDelimIdx = ($varAddrLen - 2)) do={
        :local varFieldsHead [$funcParseFields [:pick $varAddr 0 $varDelimIdx]]
        :local varFieldsTail ({})
        :for i from=0 to=(7 - [:len varFieldsTail]) do={ :set varFieldsTail ($varFieldsTail , 0) }
        :return ($varFieldsHead , $varFieldsTail)
    }

    :set varDelimIdx [:find $varAddr "::" ($varDelimIdx - 1)]

    # x::x
    :if ([:typeof $varDelimIdx] != "nil") do={
        :local varFieldsHead [$funcParseFields [:pick $varAddr 0 $varDelimIdx]]
        :local varFieldsTail [$funcParseFields [:pick $varAddr ($varDelimIdx + 2) $varAddrLen]]
        :local varFieldsMid ({})
        :for i from=0 to=(7 - [:len $varFieldsHead] - [:len $varFieldsTail]) do={ :set varFieldsMid ($varFieldsMid , 0) }
        :return ($varFieldsHead , $varFieldsMid , $varFieldsTail)
    }

    # x:x:x:x:x:x:x:x
    :return [$funcParseFields $varAddr]
}

# Return given input as an ip6 object.
#
# $1 (array): IPv6 address as an array of fields
#
# $1 (ip6, str): IPv6 prefix
# $2 (str): MAC
#
# $1 (ip6, str): IPv6 address
#
# > :put [$MakeIP6Address ({8193;3512;0;0;0;0;0;1})]
# 2001:db8::1
#
# > :put [$MakeIP6Address fe80:: "00:00:5E:00:53:01"]
# fe80::200:5eff:fe00:5301
#
# > :put [$MakeIP6Address "2001:db8::1"]
# 2001:db8::1
:global MakeIP6Address do={
    :if ([:typeof $1] = "array") do={
        :local varHexMap [:toarray "0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F"]
        :local varDigitMask {0xf000 ; 0x0f00 ; 0x00f0 ; 0x000f}
        :local varAddr ""

        :for fieldIdx from=0 to=7 do={
            :local varFieldNum ($1->$fieldIdx)

            :if ($varFieldNum != 0) do={
                :for digitIdx from=0 to=3 do={
                    :local varDigitNum (($varFieldNum & ($varDigitMask->$digitIdx)) >> (12 - $digitIdx * 4))
                    :local varDigit ($varHexMap->$varDigitNum)
                    :set varAddr ($varAddr . $varDigit)
                }
            } else={
                :set varAddr ($varAddr . "0")
            }

            :if ($fieldIdx != 7) do={ :set varAddr ($varAddr . ":") }
        }

        :return [:toip6 $varAddr]
    }

    :local argAddr [:toip6 $1]
    :if ([:typeof $argAddr] != "ip6") do={ :error "\"$1\" is invalid IPv6 address"}

    :if (([:typeof $2] = "str") and ([:len $2] = 17)) do={
        :local varPrefixFields [$MakeIP6AddressFields $1]
        :put "$varPrefixFields"
        :local varMACFields {\
            [:tonum "0x$[:pick $2 0 2]"] ;\
            [:tonum "0x$[:pick $2 3 5]"] ;\
            [:tonum "0x$[:pick $2 6 8]"] ;\
            [:tonum "0x$[:pick $2 9 11]"] ;\
            [:tonum "0x$[:pick $2 12 14]"] ;\
            [:tonum "0x$[:pick $2 15 17]"]\
        }
        :put "$varMACFields"
        :local varEUI64Fields {\
            (((($varMACFields->0) << 8) | ($varMACFields->1)) ^ 0x0200) ;\
            ((($varMACFields->2) << 8) | 0x00ff) ;\
            (0xfe00 | ($varMACFields->3)) ;\
            ((($varMACFields->4) << 8) | ($varMACFields->5))\
        }
        :put "$varMACFields"
        :local varAddrFields {\
            ($varPrefixFields->0) ;\
            ($varPrefixFields->1) ;\
            ($varPrefixFields->2) ;\
            ($varPrefixFields->3) ;\
            ($varEUI64Fields->0) ;\
            ($varEUI64Fields->1) ;\
            ($varEUI64Fields->2) ;\
            ($varEUI64Fields->3)\
        }
        :put "$varAddrFields"
        :put "$[:len $varAddrFields]"
        :return [$MakeIP6Address $varAddrFields]
    }

    :return $argAddr
}

# Return general attributes of an IPv6 address.
#
# $1 (ip6, str): IPv6 address
# [$2] (integer): subnet prefix length; defaults to 128
# 
# $1 (str, ip6-prefix): IPv6 address-prefix
#
# > :put [$ParseIP6AddressCommon 2001:db8::1/64]
# address=2001:db8::1;addressPrefix=2001:db8::/64;prefix=2001:db8::;prefixLength=64
:global ParseIP6AddressCommon do={
    :global MakeIP6PrefixMask

    :local varRawAddr [:toip6 $1]
    :if ([:typeof $varRawAddr] = "nil") do={
        :set varRawAddr [:tostr $1]
    }
    :if ([:typeof $varRawAddr] = "nil") do={ :error "\"$1\" is invalid IPv6 address" }

    :local varAddr
    :local varPrefix
    :local varPrefixLen
    :if ([:typeof $varRawAddr] = "ip6") do={
        :set varAddr $varRawAddr

        :if ([:typeof $2] != "nothing") do={
            :set varPrefixLen [:tonum $2]
        } else={
            :set varPrefixLen 128
        }

        :if ([:typeof $varPrefixLen] = "nil") do={ :error "\"$2\" is invalid subnet prefix length" }
    } else={
        :local varDelimIdx [:find $varRawAddr "/"]

        :if ([:typeof $varDelimIdx] != "nil") do={
            :set varAddr [:toip6 [:pick $varRawAddr 0 $varDelimIdx]]
            :set varPrefixLen [:tonum [:pick $varRawAddr ($varDelimIdx + 1) [:len $varRawAddr]]]
        } else={
            :set varAddr [:toip6 $varRawAddr]
            :set varPrefixLen 128
        }

        :if ([:typeof $varAddr] = "nil") do={ :error "\"$1\" is invalid IPv6 address" }
        :if ([:typeof $varPrefixLen] = "nil") do={ :error "\"$1\" is invalid IPv6 address" }
    }

    :local varPrefixMask [$MakeIP6PrefixMask $varPrefixLen]
    :local varPrefix ($varAddr & $varPrefixMask)
    :local varAddrPrefix [[:parse ":return $varAddr/$varPrefixLen"]]

    :return {"address"=$varAddr ; "addressPrefix"=$varAddrPrefix ; "prefix"=$varPrefix ; "prefixLength"=$varPrefixLen}
}

# Return detailed attributes of an IPv6 address with respect to its type.
#
# $1 (array): An array of common IPv6 attributes
#
# > :put [$ParseIP6AddressDetail [$ParseIP6AddressCommon 2001:db8::1/64]]
# fields=8193;3512;0;0;0;0;0;1;globalID=2001:db8::;interfaceID=::1;subnetID=::;subtype=gua;type=unicast
:global ParseIP6AddressDetail do={
    :global MakeIP6AddressFields
    :global MakeIP6PrefixMask
    :global MakeIP6SuffixMask
    :global MakeIP6Address

    :local argAddr ($1->"address")
    :local varFields [$MakeIP6AddressFields $argAddr]

    :if ($argAddr = ::) do={ :return {"type"="unspecified" ; "fields"=$varFields} }
    :if ($argAddr = ::1) do={ :return {"type"="loopback" ; "fields"=$varFields} }
    :if ($argAddr in fe80::/10) do={ :return {"type"="link-local" ; "fields"=$varFields} }

    :if (($argAddr in ::/96) or ($argAddr in ::ffff:0:0/96)) do={
        :local varIP4 (\
            ((($varFields->6) & 0xff00) >> 16) . "." .\
            (($varFields->6) & 255) . "." .\
            ((($varFields->7) & 0xff00) >> 16) . "." .\
            (($varFields->7) & 0x00ff)\
        )
        :local varDetail {"fields"=$varFields ; "ip4"=$varIP4}

        :if (($varFields->5) = 0xffff) do={
            :set varDetail ($varDetail , {"type"="ip4-mapped"})
        } else={
            :set varDetail ($varDetail , {"type"="ip4-compatible"})
        }

        :return $varDetail
    }

    :if ($argAddr in ff00::/8) do={
        :local varFlags ((($varFields->0) & 0x00f0) >> 4)
        :local varScope (($varFields->0) & 0x000f)
        :local varDetail {"type"="multicast" ; "fields"=$varFields ; "flags"=$varFlags}

        :if (($varFlags & 3) = 3) do={
            # 0?11 - RFC3306
            :local varPrefixLen (($varFields->1) & 0x00ff)
            :local varPrefixFields {($varFields->2) ; ($varFields->3) ; ($varFields->4) ; ($varFields->5) ; 0 ; 0 ; 0; 0 }
            :local varPrefixMask [$MakeIP6PrefixMask $varPrefixLen]
            :local varPrefix ([$MakeIP6Address $varPrefixFields] & $varPrefixMask)
            :local varGroupID (($varFields->6 << 16) | $varFields->7)
            :set varDetail ($varDetail , {"subnetPrefix"=$varPrefix ; "subnetPrefixLength"=$varPrefixLen ; "groupID"=$varGroupID})

            :if ($varFlags & 4) do={
                # 0111 - RFC3956
                :local varRIIDRaw ((($varFields->1) & 0x0f00) >> 8)
                :local varRIIDFields {0 ; 0 ; 0 ; 0 ; 0 ; 0 ; 0 ; $varRIIDRaw}
                :local varRIID ($varPrefix | [$MakeIP6Address $varRIIDFields])
                :set varDetail ($varDetail , {"RIID"=$varRIID})
            }

            :if ($varPrefixLen = 0) do={
                :set varDetail ($varDetail , {"SSM"=yes})
            } else={
                :set varDetail ($varDetail , {"SSM"=no})
            }
        } else={
            :local varGroupID ($argAddr & [$MakeIP6SuffixMask 112])
            :set varDetail ($varDetail , {"groupID"=$varGroupID})
        }

        :return $varDetail
    }

    :local subtype
    :local globalID
    :local subnetID ::
    :local interfaceID ($argAddr & [$MakeIP6SuffixMask 64])
    :if ($argAddr in fc00::/7) do={
        # RFC4193
        :set subtype "ula"

        :local globalIDFields {(($varFields->0) & 0x00ff) ; ($varFields->1) ; ($varFields->2) ; 0 ; 0 ; 0 ; 0 ; 0}
        :set globalID [$MakeIP6Address $globalIDFields]

        :local subnetIDFields {0 ; 0 ; 0 ; ($varFields->3) ; 0 ; 0 ; 0 ; 0}
        :set subnetID [$MakeIP6Address $subnetIDFields]
    } else={
        #RFC3587
        :set subtype "gua"

        :local globalIDLen ($1->"prefixLength")
        :set globalID ($1->"prefix")

        :local subnetIDLen (64 - $globalIDLen)
        :if ($subnetIDLen > 0) do={
            :set subnetID ($varAddr & (~([$MakeIP6PrefixMask $globalIDLen])) & (~([$MakeIP6SuffixMask 64])))
        }
    }
    
    :return {"type"="unicast" ; "subtype"=$subtype ; "fields"=$varFields ; "globalID"=$globalID ; "subnetID"=$subnetID ; "interfaceID"=$interfaceID}
}

# See usage
:global ParseIP6Address do={
    :global ParseIP6AddressCommon
    :global ParseIP6AddressDetail

    :local varCommon [$ParseIP6AddressCommon $1 $2]

    :if ([:typeof $detail] != "nothing") do={
        :if ([[:parse "[:tobool $detail]"]]) do={
            :local varDetail [$ParseIP6AddressDetail $varCommon]
            :set varCommon ($varCommon , {"detail"=$varDetail})
        } 
    }

    :return $varCommon
}

Also available as a GitHub gist for version tracking.

Keywords: extract ipv6 address, extract ipv6 prefix, slaac address, eui64 address

Some scripting tips (v6/v7):

:toip6 returns a valid IP address and if not valid it returns “nil” which is an :if compare the same as “false/invalid”, first a direct usage and the second one is a indirect usage.

FROM:
if ([:typeof $address] = "nil") do={ :error "\"$1\" is invalid IPv6 address" }
TO USING STATUS:
:if [:toip6 $address] do={ :error "\"$1\" is invalid IPv6 address" }

Second:

FROM:
:local rawAddress [:toip6 $1]; :if ([:typeof $rawAddress] = "nil") do={:set rawAddress [:tostr $1]}

TO;
:local rawAddress [:toip6 $1]; :if $rawAddress do={:set rawAddress [:tostr $1]}

Third I don’t follow what it is doing. :toip6 does handle strings so why a second try?:

FROM:
    :local rawAddress [:toip6 $1]
    :if ([:typeof $rawAddress] = "nil") do={
        :set rawAddress [:tostr $1]
    }
    :if ([:typeof $rawAddress] = "nil") do={ :error "\"$1\" is invalid IPv6 address" }
TO:    
    :local rawAddress [:toip6 $1]
    :if $rawAddress do={} else={:set rawAddress [:tostr $1] }; # you can't negate an IP-address here so that is why using the do={} else=:set...$1]} construction
    :if [:typeof $rawAddress] do={} else={:error "\"$1\" is invalid IPv6 address" }

Thank you for the comments. Didn’t know that RouterOS evaluated nil as false.


:toip6 will fail is an Address Prefix string. The script also handles ip6-prefix types by converting them to str. In anticipation of Mikrotik adding more types to represent IPv6 addresses the script is written to proof against by converting whatever input to str.

nil is a bit more complex, when I thought about it later:

 > :local a false; :put [:typeof $a]    
bool
 > :local a ; :put [:typeof $a]     
nothing
 > :local a nil ; :put [:typeof $a]     
str
 > :local a [:toip 192.168.88.999]; :put [:typeof $a]
nil
 > :local a [:toip 192.168.88.1]; :put [:typeof $a]   
ip

So to me “nil” is an result, the result being invalid. Also notice that, a not set variable, is displayed at “nothing”.

Then seeing them as true/false is not correct. Seeing them as result being valid/invalid is more appropriate.

I can’t find nothing back under data types: https://help.mikrotik.com/docs/display/ROS/Scripting#Scripting-Datatypes assuming MT have mixed both up and then forgot about nothing.

I used “nothing” to detect unset arguments.


IMO worths a bug request to improve the documentation.

This assumption by me was wrong. The value is treated as a boolean and then 0 = false and any other number is true.

All types of RouterOS, including those that have not yet been discovered from other users…

The usual ones
nil
nothing
str (string)
bool (truth value)
num (integer number)
time (time interval)
ip (ip address)
ip-prefix (ip prefix)
ip6 (ipv6 address)
ip6-prefix (ipv6 prefix)
id (internal number)
array
code
lookup

Recently discovered
op (operation)

Not discovered
apireq (api request)
exclamation
cmd
iterator
backreference

:global test “2333:7777:333:1100::/56”
:put [:typeof $test]
:put $test

:execute “:global test [[:parse ":return $test"]]”
:put [:typeof $test]
:put $test

Nice trick! Also love the usage of the [code2] tag. Do you have a reference for all other tags supported by this forum engine?

If you had read mine :scissors: Rextended Fragments of Snippets you would have seen it before… both…


Read again previous post…

Which snippet are you referring to?

http://forum.mikrotik.com/t/persistent-environment-variables/145326/6

Revision 2:

  • Address is now represented both as ip6 and ip6-prefix
  • When the detail argument is passed as “detail=yes”, the output will provide address type, type-specific properties and address representation as an array of 16bit integers

With actual version:
[…] - When the detail argument variable is passed as with any value, like “detail=yesanything”, the output will […]


:if ([:typeof $detail] != "nothing") do={
# to keep your description consistent it must be
:if ([:typeof $detail] = "yes") do={

# or with Rex-style, just write "detail" as any parameter $2 or $3
:if ("$2$3"~"detail") do={

This doesn’t concur with my testing. "[[:parse “[:tobool $detail]”]] will evaluate to boolean yes only for >0, “>0”, true, “true”, yes and “yes”.

You do not catch the point: Why do two useless passages instead only one?

Revision 3:

  • $MakeIP6Address learned to generate an IPv6 address from a 48-bit MAC address via EUI-64

Revision 4:

  • Reworked as a module for routeros-scripts and moved to a separate repository
  • MakeIP6Address was split into MakeIP6AddressFromFields and MakeIP6AddressFromEUI64
  • Added MakeIP6AddressFromNPT that makes IPv6 address by translating its prefix as specified in RFC6296