【问题标题】:How to validate a IPv6 address format with shell?如何使用 shell 验证 IPv6 地址格式?
【发布时间】:2014-11-07 08:07:33
【问题描述】:

在我的 shell 中,我需要检查一个字符串是否是一个有效的 IPv6 地址。

我找到了两种方法,对我来说都不够理想。

一个是http://twobit.us/2011/07/validating-ip-addresses/,而我想知道对于这样一个常见的需求,它是否必须如此复杂。

另一个是expand ipv6 address in shell script,这很简单,但对于 Linux 的主要发行版,sipcalc 不是一个常见的默认实用程序。

所以我的问题是,有没有一种简单的方法或实用程序可以使用 shell 验证 IPv6 地址?

提前致谢。

【问题讨论】:

  • 除非安装了一些外部二进制实用程序,否则我相信shell脚本解决方案本身(您提到的解决方案1)已经太简单了。
  • 二进制实用程序只有在普通 Linux 发行版中很常见时才是好的。
  • 这只是为了检查它是否有效或将其转换为有效地址?
  • @Jidder 是的,仅用于地址格式。
  • 有一些非常复杂的 IPv6 地址组合,您可能希望也可能不希望将其视为有效。每个版本都有未压缩、压缩和混合版本;加入全局单播、多播、本地链接、本地唯一、IPv4 映射等。您需要决定什么是您可以接受的(您接受还是拒绝符合格式的保留、未使用的地址?)。有完整的 IPv6 正则表达式库,这可能是您的最佳选择。

标签: bash shell validation ipv6


【解决方案1】:

这是 POSIX 兼容的 shell 脚本中的一个解决方案,它使用可选的子网掩码处理 IPv4 和 IPv6 地址。要测试应该有子网掩码的IP,只需在执行测试时传递一个虚拟IP。看起来代码很多,但它应该比使用外部程序(如 grep 或可能分叉的脚本)快得多。

压缩到 :: 的单个 IPv6 零组将被视为无效。强烈建议不要使用这种表示,但在技术上是正确的。如果您希望允许此类地址,代码中有一条注释说明如何更改此行为。

#!/bin/sh
set -e

# return nonzero unless $1 contains only digits, leading zeroes not allowed
is_numeric() {
    case "$1" in
        "" | *[![:digit:]]* | 0[[:digit:]]* ) return 1;;
    esac
}

# return nonzero unless $1 contains only hexadecimal digits
is_hex() {
    case "$1" in
        "" | *[![:xdigit:]]* ) return 1;;
    esac
}

# return nonzero unless $1 is a valid IPv4 address with optional trailing subnet mask in the format /<bits>
is_ip4() {

    # fail if $1 is not set, move it into a variable so we can mangle it 
    [ -n "$1" ] || return
    IP4_ADDR="$1"

    # handle subnet mask for any address containing a /
    case "$IP4_ADDR" in
        *"/"* ) # set $IP4_GROUP to the number of bits (the characters after the last /)
                IP4_GROUP="${IP4_ADDR##*"/"}"

                # return failure unless $IP4_GROUP is a positive integer less than or equal to 32
                is_numeric "$IP4_GROUP" && [ "$IP4_GROUP" -le 32 ] || return

                # remove the subnet mask from the address
                IP4_ADDR="${IP4_ADDR%"/$IP4_GROUP"}";;
    esac

    # backup current $IFS, set $IFS to . as that's what separates digit groups (octets)
    IP4_IFS="$IFS"; IFS="."

    # initialize count
    IP4_COUNT=0

    # loop over digit groups
    for IP4_GROUP in $IP4_ADDR ;do  
        # return failure if group is not numeric or if it is greater than 255
        ! is_numeric "$IP4_GROUP" || [ "$IP4_GROUP" -gt 255 ] && IFS="$IP4_IFS" && return 1

        # increment count
        IP4_COUNT=$(( IP4_COUNT + 1 ))

        # the following line will prevent the loop continuing to run for invalid addresses with many occurrences of .
        # this makes no difference to the result, but may improve performance when validating many such invalid strings
        [ "$IP4_COUNT" -le 4 ] || break
    done

    # restore $IFS
    IFS="$IP4_IFS"

    # return success if there are 4 digit groups, otherwise return failure
    [ "$IP4_COUNT" -eq 4 ]
}

# return nonzero unless $1 is a valid IPv6 address with optional trailing subnet mask in the format /<bits>
is_ip6() {
    # fail if $1 is not set, move it into a variable so we can mangle it 
    [ -n "$1" ] || return
    IP6_ADDR="$1"

    # handle subnet mask for any address containing a /
    case "$IP6_ADDR" in
        *"/"* ) # set $IP6_GROUP to the number of bits (the characters after the last /)
                IP6_GROUP="${IP6_ADDR##*"/"}"

                # return failure unless $IP6_GROUP is a positive integer less than or equal to 128
                is_numeric "$IP6_GROUP" && [ "$IP6_GROUP" -le 128 ] || return

                # remove the subnet mask from the address
                IP6_ADDR="${IP6_ADDR%"/$IP6_GROUP"}";;
    esac

    # perform some preliminary tests and check for the presence of ::
    case "$IP6_ADDR" in
        # failure cases
        # *"::"*"::"*  matches multiple occurrences of ::
        # *":::"*      matches three or more consecutive occurrences of :
        # *[^:]":"     matches trailing single :
        # *"."*":"*    matches : after .
        *"::"*"::"* | *":::"* | *[^:]":" | *"."*":"* ) return 1;;

        *"::"* ) # set flag $IP6_EXPANDED to true, to allow for a variable number of digit groups
                 IP6_EXPANDED=0

                 # because :: should not be used for remove a single zero group we start the group count at 1 when :: exists
                 # NOTE This is a strict interpretation of the standard, applications should not generate such IP addresses but (I think)
                 #      they are in fact technically valid. To allow addresses with single zero groups replaced by :: set $IP6_COUNT to 
                 #      zero after this case statement instead
                 IP6_COUNT=1;; 

        *      ) # set flag $IP6_EXPANDED to false, to forbid a variable number of digit groups
                 IP6_EXPANDED=""

                 # initialize count
                 IP6_COUNT=0;;
    esac
    # backup current $IFS, set $IFS to : to delimit digit groups
    IP6_IFS="$IFS"; IFS=":"

    # loop over digit groups
    for IP6_GROUP in $IP6_ADDR ;do
        # if this is an empty group then increment count and process next group
        [ -z "$IP6_GROUP" ] && IP6_COUNT=$(( IP6_COUNT + 1 )) && continue

        # handle dotted quad notation groups
        case "$IP6_GROUP" in
            *"."* ) # return failure if group is not a valid IPv4 address
                    # NOTE a subnet mask is added to the group to ensure we are matching addresses only, not ranges
                    ! is_ip4 "$IP6_GROUP/1" && IFS="$IP6_IFS" && return 1

                    # a dotted quad refers to 32 bits, the same as two 16 bit digit groups, so we increment the count by 2
                    IP6_COUNT=$(( IP6_COUNT + 2 ))

                    # we can stop processing groups now as we can be certain this is the last group, : after . was caught as a failure case earlier
                    break;;
        esac

        # if there are more than 4 characters or any character is not a hex digit then return failure
        [ "${#IP6_GROUP}" -gt 4 ] || ! is_hex "$IP6_GROUP" && IFS="$IP6_IFS" && return 1

        # increment count
        IP6_COUNT=$(( IP6_COUNT + 1 ))

        # the following line will prevent the loop continuing to run for invalid addresses with many occurrences of a single :
        # this makes no difference to the result, but may improve performance when validating many such invalid strings
        [ "$IP6_COUNT" -le 8 ] || break
    done

    # restore $IFS
    IFS="$IP6_IFS"

    # if this address contained a :: and it has less than or equal to 8 groups then return success 
    [ "$IP6_EXPANDED" = "0" ] && [ "$IP6_COUNT" -le 8 ] && return

    # if this address contained exactly 8 groups then return success, otherwise return failure
    [ "$IP6_COUNT" -eq 8 ]
}

这里有一些测试。

# tests
TEST_PASSES=0
TEST_FAILURES=0
for TEST_IP in 0.0.0.0 255.255.255.255 1.2.3.4/1 1.2.3.4/32 12.12.12.12 123.123.123.123 101.201.201.109 ;do
    ! is_ip4 "$TEST_IP" && printf "IP4 test failed, test case '%s' returned invalid\n" "$TEST_IP" && TEST_FAILURES=$(( TEST_FAILURES + 1 )) || TEST_PASSES=$(( TEST_PASSES + 1 )) 
done
for TEST_IP in ::1 ::1/128 ::1/0 ::1234 ::bad ::12 1:2:3:4:5:6:7:8 1234:5678:90ab:cdef:1234:5678:90ab:cdef \
               1234:5678:90ab:cdef:1234:5678:90ab:cdef/127 1234:5678:90ab::5678:90ab:cdef/64 f:1234:c:ba:240::1 \
               1:2:3:4:5:6:1.2.3.4 ::1.2.3.4 ::1.2.3.4/0 ::ffff:1.2.3.4 ;do
    ! is_ip6 "$TEST_IP" && printf "IP6 test failed, test case '%s' returned invalid\n" "$TEST_IP" && TEST_FAILURES=$(( TEST_FAILURES + 1 )) || TEST_PASSES=$(( TEST_PASSES + 1 )) 
done
for TEST_IP in junk . / 0 -1.0.0.0 1.2.c.0 a.0.0.0 " 1.2.3.4" "1.2.3.4 " " " 01.0.0.0 09.0.0.0 0.0.0.01 \
               0.0.0.09 0.09.0.0.0 0.01.0.0 0.0.01.0 0.0.0.a 0.0.0 .0.0.0.0 256.0.0.0 0.0.0.256 "" 0 1 12 \
               123 1.2.3.4/s 1.2.3.4/33 1.2.3.4/1/1 ;do
    is_ip4 "$TEST_IP" && printf "IP4 test failed, test case '%s' returned valid\n" "$TEST_IP" && TEST_FAILURES=$(( TEST_FAILURES + 1 )) || TEST_PASSES=$(( TEST_PASSES + 1 )) 
done
for TEST_IP in junk "" : / :1 ::1/ ::1/1/1 :::1 ::1/129 ::12345 ::bog ::1234:345.234.0.0 ::sdf.d ::1g2 \
               1:2:3:44444:5:6:7:8 1:2:3:4:5:6:7 1:2:3:4:5:6:7:8/1c1 1234:5678:90ab:cdef:1234:5678:90ab:cdef:1234/64 \
               1234:5678:90ab:cdef:1234:5678::cdef/64  ::1.2.3.4:1 1.2.3.4:: ::1.2.3.4j ::1.2.3.4/ ::1.2.3.4:junk ::1.2.3.4.junk ;do
    is_ip6 "$TEST_IP" && printf "IP6 test failed, test case '%s' returned valid\n" "$TEST_IP" && TEST_FAILURES=$(( TEST_FAILURES + 1 )) || TEST_PASSES=$(( TEST_PASSES + 1 )) 
done
printf "test complete, %s passes and %s failures\n" "$TEST_PASSES" "$TEST_FAILURES"

【讨论】:

  • @mklement0我认为除了 posh 之外的每个现代 shell 都支持本地,但是是的,它不是严格的 POSIX。清除 $Group 和 $Expanded 并确保 $IFS 不会被破坏是一件简单的事情
【解决方案2】:

大多数发行版都预装了包 iproute2(名称可能不同)。所以可以依靠命令ip查询路由表:

ip -6 route get <probe_addr>/128 >/dev/null 2>&1 

即使在没有适当路由的机器上,当探针采用有效的 v6 语法时,它也会提供 rc=0。

【讨论】:

    【解决方案3】:

    第一个链接中的代码不是特别优雅,但是模数样式修复,我认为您不能简化太多(正如评论中所指出的,它可能已经太简单了)。该规范很复杂,并要求提供许多可选功能,这对最终用户来说很好,但对实施者来说却很麻烦。

    您可能会找到一个通用脚本语言的库,该库将这个逻辑正确地封装在一个库中。我的想法是 Python,Python 3.3 确实包含一个名为 ipaddress 的标准模块;对于旧版本,请尝试类似

    #!/usr/bin/env python
    import socket
    import sys
    try:
        socket.inet_pton(socket.AF_INET6, sys.argv[1])
        result=0
    except socket.error:
        result=1
    sys.exit(result)
    

    另见Checking for IP addresses

    【讨论】:

    • 感谢您的回答。这是可以接受的,但我仍然期待不涉及另一种语言的另一种解决方案。
    猜你喜欢
    • 2023-04-01
    • 1970-01-01
    • 2015-11-26
    • 2011-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-10
    相关资源
    最近更新 更多