diff --git a/docs/shell-ip-address.md b/docs/shell-ip-address.md index e7b3524..d4d3e0f 100644 --- a/docs/shell-ip-address.md +++ b/docs/shell-ip-address.md @@ -2,7 +2,7 @@ shell-ip-address(3) # NAME -ipv4_ip_subnet, ipv4_mask2prefix, ipv4_prefix2mask, ipv4_ptonx, ipv6_addr_type, ipv6_ptonx, valid_ipv4 - functions to manipulate IP addresses +ipv4_ip_subnet, ipv4_mask2prefix, ipv4_prefix2mask, ipv4_ptonx, ipv6_addr_type, ipv6_ip_matches, ipv6_ptonx, valid_ipv4 - functions to manipulate IP addresses # SYNOPSIS @@ -13,12 +13,13 @@ ipv4_ip_subnet, ipv4_mask2prefix, ipv4_prefix2mask, ipv4_ptonx, ipv6_addr_type, - ipv4_ptonx ipaddr - ipv6_addr_type ipaddr +- ipv6_ip_matches ipaddr prefix - ipv6_ptonx ipaddr # DESCRIPTION ## ipv4_ip_subnet -Function checks that IP address is in subnet. +Function checks that IPv4 address is in subnet. Example: ``` @@ -133,6 +134,23 @@ done The intended purpose of *ipv6_addr_type* is to prevent misuse of special-use addresses. For example, since link-local unicast addresses only make sense within a link and are incomplete without a scope identifier, it generally makes no sense to specify them in DNS AAAA records. +## ipv6_ip_matches +Checks that an IPv6 address is in a prefix (IOW, belongs in a range with common leading bits). + +Example: +``` +ipv6_ip_matches 3fff:e:b:1::2 3fff:e:b:1::/64; echo res=$? +res=0 + +ipv6_ip_matches 3fff:e:b:3::2 3fff:e:b:1::/64; echo res=$? +res=1 + +ipv6_ip_matches 3fff:e:b:3::2 3fff:e:b::/48; echo res=$? +res=0 +``` + +Remember that an IPv6 prefix (netmask of leading 1 bits), unlike an IPv4 prefix, does not necessarily have routing significance; it might or might not be reachable "on-link" in any subnet. + ## ipv6_ptonx This function interprets the given option value as an IPv6 address similarly to inet_pton(3), and outputs each octet in network byte order as 16 adjacent 2-digit hexadecimal numbers. This form is useful to perform bitwise operations on the address. diff --git a/shell-ip-address b/shell-ip-address index 7a6a1e7..66bc1b4 100644 --- a/shell-ip-address +++ b/shell-ip-address @@ -108,7 +108,7 @@ __len2mask_32() printf '%s' "$mask" } -### Checks that IP address is in subnet +### Checks that IPv4 address is in subnet ### Usage example: ### ipv4_ip_subnet 172.16.1.2 172.16.1.0/24; echo res=$? ### res=0 @@ -345,4 +345,56 @@ ipv6_addr_type() return 0 } +### Checks that IPv6 address is in a prefix. +### Usage example: +### ipv6_ip_matches 3fff:e:b:1::2 3fff:e:b:1::/64; echo res=$? +### res=0 +### +### ipv6_ip_matches 3fff:e:b:3::2 3fff:e:b:1::/64; echo res=$? +### res=1 +### +### ipv6_ip_matches 3fff:e:b:3::2 3fff:e:b::/48; echo res=$? +### res=0 +ipv6_ip_matches() +{ + local addr pref preflen + addr="${1-}"; shift + pref="${1-}"; shift + preflen="${pref##*/}" + + [ -n "$preflen" ] || + return 2 + [ "$preflen" = 0 ] || shell_var_is_number "$preflen" || + return 2 + [ "$preflen" -le 128 ] || + return 2 + + local hex_addr hex_pref hex_mask + + hex_addr="$(ipv6_ptonx "$addr")" && + hex_pref="$(ipv6_ptonx "${pref%%/*}")" || + return 2 + + local i_addr i_pref w_addr w_pref + while [ "$preflen" -gt 32 ]; do + i_addr="${hex_addr#????????}" + i_pref="${hex_pref#????????}" + w_addr="${hex_addr%$i_addr}" + w_pref="${hex_pref%$i_pref}" + [ "$w_pref" = "$w_addr" ] || + return 1 + + hex_addr="$i_addr" + hex_pref="$i_pref" + preflen="$(($preflen - 32))" + done + i_addr="${hex_addr#????????}" + i_pref="${hex_pref#????????}" + w_addr="${hex_addr%$i_addr}" + w_pref="${hex_pref%$i_pref}" + hex_mask="$(__len2mask_32 "$preflen")" + [ "$((0x$w_pref & $hex_mask))" -eq "$((0x$w_addr & $hex_mask))" ] || + return 1 +} + fi #__included_shell_ip_address diff --git a/tests/ip_address b/tests/ip_address index 1033365..567a223 100644 --- a/tests/ip_address +++ b/tests/ip_address @@ -890,3 +890,183 @@ ip_address_test6t21() { # UnitTest assertFalse "malformed address: $repr" $rc assertNull "empty output" "$(ipv6_addr_type "$repr")" } + +ip_address_test_match600() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:7::23:443 + local prefix=::/0 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "matches 'default route': $addr" $rc +} + +ip_address_test_match602() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=::ffff:c000:2 + local prefix=::/64 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "enough common bits: $addr -> $prefix" $rc +} + +ip_address_test_match603() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=::ffff:c000:2 + local prefix=::ffff:0:0/96 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "$addr -> $prefix" $rc +} + +ip_address_test_match604() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:560a::c000:204 + local prefix=2001:db8:7:5608::/61 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "non-4bit-aligned prefix: $addr -> $prefix" $rc +} + +ip_address_test_match605() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:560a::c000:204 + local prefix=2001:db8:7:5600::/60 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "non-32bit-aligned prefix: $addr -> $prefix" $rc +} + +ip_address_test_match606() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:560a::c000:204 + local prefix=2001:db8:7:5000::/52 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "non-32bit-aligned prefix: $addr -> $prefix" $rc +} + +ip_address_test_match607() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:500a::c000:205 + local prefix=2001:db8:7:500a::c000:204/127 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "127-bit prefix: $addr -> $prefix" $rc +} + +ip_address_test_match608() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:500a::c000:204 + local prefix=2001:db8:7:500a::c000:204/128 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "128-bit prefix: $addr -> $prefix" $rc +} + +ip_address_test_match616() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=3fff:f:e:4::c + local prefix=3fff:f:e:4::/64 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "$addr -> $prefix" $rc +} + +ip_address_test_match617() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=3fff:f:e:4::c + local prefix=3fff:f:e:4::/48 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertTrue "$addr -> $prefix" $rc +} + +ip_address_test_match620() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=::ffff:c000:2 + local prefix=::/96 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertFalse "common bits too short: $addr -> $prefix" $rc +} + +ip_address_test_match621() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001::ffff:c000:2 + local prefix=::ffff:0:0/96 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertFalse "common low bits: $addr -> $prefix" $rc +} + +ip_address_test_match624() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:560a::c000:204 + local prefix=2001:db8::/60 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertFalse "! $addr -> $prefix" $rc +} + +ip_address_test_match636() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=3fff:f:e:4::c + local prefix=3fff:f:e:1::/64 + ipv6_ip_matches "$addr" "$prefix" || rc=1 + assertFalse "! $addr -> $prefix" $rc +} + +ip_address_test_match640() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=xxx + local prefix=2002::/16 + ipv6_ip_matches "$addr" "$prefix" || rc=$? + assertEquals "garbage addr: $addr" 2 $rc +} + +ip_address_test_match641() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:e8::c0 + local prefix=yyy/16 + ipv6_ip_matches "$addr" "$prefix" || rc=$? + assertEquals "garbage prefix: $prefix" 2 $rc +} + +ip_address_test_match642() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:e8::c0 + local prefix=3fff:1::/zzz + ipv6_ip_matches "$addr" "$prefix" || rc=$? + assertEquals "garbage prefix length: $prefix" 2 $rc +} + +ip_address_test_match643() { # UnitTest + . ../shell-ip-address + + local rc=0 + local addr=2001:db8:7:e8::c0 + local prefix=3fff:1::/100500 + ipv6_ip_matches "$addr" "$prefix" || rc=$? + assertEquals "garbage prefix length: $prefix" 2 $rc +}