1#!/bin/bash 2# Copyright 2021 Google LLC 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16[ -n "${network_init-}" ] && return 17 18mac_to_bytes() { 19 local -n bytes="$1" 20 local str="$2" 21 22 # Verify that the MAC is Valid 23 [[ "$str" =~ ^[[:xdigit:]]{1,2}(:[[:xdigit:]]{1,2}){5}$ ]] || return 24 25 # Split the mac into hex bytes 26 local oldifs="$IFS" 27 IFS=: 28 local byte 29 for byte in $str; do 30 bytes+=("0x$byte") 31 done 32 IFS="$oldifs" 33} 34 35mac_to_eui48() { 36 local mac_bytes=(0 0 0 0 0 0 0 0 0 0) 37 mac_to_bytes mac_bytes "$1" || return 38 39 # Return the EUI-64 bytes in the IPv6 format 40 ip_bytes_to_str mac_bytes 41} 42 43mac_to_eui64() { 44 local mac_bytes=() 45 mac_to_bytes mac_bytes "$1" || return 46 mid_byte1="$2" 47 mid_byte2="$3" 48 [[ -z "$mid_byte1" ]] && mid_byte1="0xff" 49 [[ -z "$mid_byte2" ]] && mid_byte2="0xfe" 50 51 # Using EUI-64 conversion rules, create the suffix bytes from MAC bytes 52 # Invert bit-1 of the first byte, and insert 0xfffe in the middle. 53 # Also support customized mid bytes. 54 # shellcheck disable=SC2034 55 local suffix_bytes=( 56 0 0 0 0 0 0 0 0 57 $((mac_bytes[0] ^ 2)) 58 "${mac_bytes[@]:1:2}" 59 $(( mid_byte1 )) $(( mid_byte2 )) 60 "${mac_bytes[@]:3:3}" 61 ) 62 63 # Return the EUI-64 bytes in the IPv6 format 64 ip_bytes_to_str suffix_bytes 65} 66 67ip_to_bytes() { 68 local -n bytes_out="$1" 69 local str="$2" 70 71 local bytes=() 72 local oldifs="$IFS" 73 # Heuristic for V4 / V6, validity will be checked as it is parsed 74 if [[ "$str" == *.* ]]; then 75 # Ensure we don't start or end with IFS 76 [ "${str:0:1}" != '.' ] || return 1 77 [ "${str: -1}" != '.' ] || return 1 78 79 local v 80 # Split IPv4 address into octets 81 IFS=. 82 for v in $str; do 83 # IPv4 digits are always decimal numbers 84 if ! [[ "$v" =~ ^[0-9]+$ ]]; then 85 IFS="$oldifs" 86 return 1 87 fi 88 # Each octet is a single byte, make sure the number isn't larger 89 if (( v > 0xff )); then 90 IFS="$oldifs" 91 return 1 92 fi 93 bytes+=("$v") 94 done 95 # IPv4 addresses must have all 4 bytes present 96 if (( "${#bytes[@]}" != 4 )); then 97 IFS="$oldifs" 98 return 1 99 fi 100 else 101 # Ensure we bound the padding in an outer byte for 102 # IFS splitting to work correctly 103 [ "${str:0:2}" = '::' ] && str="0$str" 104 [ "${str: -2}" = '::' ] && str="${str}0" 105 106 # Ensure we don't start or end with IFS 107 [ "${str:0:1}" != ':' ] || return 1 108 [ "${str: -1}" != ':' ] || return 1 109 110 # Stores the bytes that come before ::, if it exists 111 local bytesBeforePad=() 112 local v 113 # Split the Address into hextets 114 IFS=: 115 for v in $str; do 116 # Handle ::, which translates to an empty string 117 if [ -z "$v" ]; then 118 # Only allow a single :: sequence in an address 119 if (( "${#bytesBeforePad[@]}" > 0 )); then 120 IFS="$oldifs" 121 return 1 122 fi 123 # Store the already parsed upper bytes separately 124 # This allows us to calculate and insert padding 125 bytesBeforePad=("${bytes[@]}") 126 bytes=() 127 continue 128 fi 129 # IPv6 digits are always hex 130 if ! [[ "$v" =~ ^[[:xdigit:]]+$ ]]; then 131 IFS="$oldifs" 132 return 1 133 fi 134 # Ensure the number is no larger than a hextet 135 v="0x$v" 136 if (( v > 0xffff )); then 137 IFS="$oldifs" 138 return 1 139 fi 140 # Split the hextet into 2 bytes 141 bytes+=($(( v >> 8 ))) 142 bytes+=($(( v & 0xff ))) 143 done 144 # If we have ::, add padding 145 if (( "${#bytesBeforePad[@]}" > 0 )); then 146 # Fill the middle bytes with padding and store in `bytes` 147 while (( "${#bytes[@]}" + "${#bytesBeforePad[@]}" < 16 )); do 148 bytesBeforePad+=(0) 149 done 150 bytes=("${bytesBeforePad[@]}" "${bytes[@]}") 151 fi 152 # IPv6 addresses must have all 16 bytes present 153 if (( "${#bytes[@]}" != 16 )); then 154 IFS="$oldifs" 155 return 1 156 fi 157 fi 158 159 IFS="$oldifs" 160 # shellcheck disable=SC2034 161 bytes_out=("${bytes[@]}") 162} 163 164ip_bytes_to_str() { 165 # shellcheck disable=SC2178 166 local -n bytes="$1" 167 168 if (( "${#bytes[@]}" == 4 )); then 169 printf '%d.%d.%d.%d\n' "${bytes[@]}" 170 elif (( "${#bytes[@]}" == 16 )); then 171 # Track the starting position of the longest run of 0 hextets (2 bytes) 172 local longest_i=0 173 # Track the size of the longest run of 0 hextets 174 local longest_s=0 175 # The index of the first 0 byte in the current run of zeros 176 local first_zero=0 177 local i 178 # Find the location of the longest run of zero hextets, preferring same 179 # size runs later in the address. 180 for (( i=0; i<=16; i+=2 )); do 181 # Terminate the run of zeros if we are at the end of the array or 182 # have a non-zero hextet 183 if (( i == 16 || bytes[i] != 0 || bytes[$((i+1))] != 0 )); then 184 local s=$((i - first_zero)) 185 if (( s >= longest_s )); then 186 longest_i=$first_zero 187 longest_s=$s 188 fi 189 first_zero=$((i+2)) 190 fi 191 done 192 # Build the address string by each hextet 193 for (( i=0; i<16; i+=2 )); do 194 # If we encountered a run of zeros, add the necessary :: at the end 195 # of the string. If not at the end, a single : is added since : is 196 # printed to subsequent hextets already. 197 if (( i == longest_i )); then 198 (( i += longest_s-2 )) 199 printf ':' 200 # End of string needs to be :: 201 if (( i == 14 )); then 202 printf ':' 203 fi 204 else 205 # Prepend : to all hextets except the first for separation 206 if (( i != 0 )); then 207 printf ':' 208 fi 209 printf '%x' $(( (bytes[i]<<8) | bytes[$((i+1))])) 210 fi 211 done 212 printf '\n' 213 else 214 echo "Invalid IP Bytes: ${bytes[*]}" >&2 215 return 1 216 fi 217} 218 219ip_pfx_concat() { 220 local pfx="$1" 221 local sfx="$2" 222 223 # Parse the prefix 224 if ! [[ "$pfx" =~ ^([0-9a-fA-F:.]+)/([0-9]+)$ ]]; then 225 echo "Invalid IP prefix: $pfx" >&2 226 return 1 227 fi 228 local addr="${BASH_REMATCH[1]}" 229 local cidr="${BASH_REMATCH[2]}" 230 231 # Ensure prefix doesn't have too many bytes 232 local pfx_bytes=() 233 if ! ip_to_bytes pfx_bytes "$addr"; then 234 echo "Invalid IP prefix: $pfx" >&2 235 return 1 236 fi 237 if (( ${#pfx_bytes[@]}*8 < cidr )); then 238 echo "Prefix CIDR too large" >&2 239 return 1 240 fi 241 # CIDR values might partially divide a byte so we need to mask out 242 # only the part of the byte we want to check for emptiness 243 if (( (pfx_bytes[cidr/8] & ~(~0 << (8-cidr%8))) != 0 )); then 244 echo "Invalid byte $((cidr/8)): $pfx" >&2 245 return 1 246 fi 247 local i 248 # Check the rest of the whole bytes to make sure they are empty 249 for (( i=cidr/8+1; i<${#pfx_bytes[@]}; i++ )); do 250 if (( pfx_bytes[i] != 0 )); then 251 echo "Byte $i not 0: $pfx" >&2 252 return 1 253 fi 254 done 255 256 # Validate the suffix 257 local sfx_bytes=() 258 if ! ip_to_bytes sfx_bytes "$sfx"; then 259 echo "Invalid IPv6 suffix: $sfx" >&2 260 return 1 261 fi 262 if (( "${#sfx_bytes[@]}" != "${#pfx_bytes[@]}" )); then 263 echo "Suffix not the same family as prefix: $pfx $sfx" >&2 264 return 1 265 fi 266 # Check potential partially divided bytes for emptiness in the upper part 267 # based on the division specified in CIDR. 268 if (( (sfx_bytes[cidr/8] & (~0 << (8-cidr%8))) != 0 )); then 269 echo "Invalid byte $((cidr/8)): $sfx" >&2 270 return 1 271 fi 272 local i 273 # Check the bytes before the CIDR for emptiness to ensure they don't overlap 274 for (( i=0; i<cidr/8; i++ )); do 275 if (( sfx_bytes[i] != 0 )); then 276 echo "Byte $i not 0: $sfx" >&2 277 return 1 278 fi 279 done 280 281 out_bytes=() 282 for (( i=0; i<${#pfx_bytes[@]}; i++ )); do 283 out_bytes+=($(( pfx_bytes[i] | sfx_bytes[i] ))) 284 done 285 echo "$(ip_bytes_to_str out_bytes)/$cidr" 286} 287 288ip_pfx_to_cidr() { 289 [[ "$1" =~ ^[0-9a-fA-F:.]+/([0-9]+)$ ]] || return 290 echo "${BASH_REMATCH[1]}" 291} 292 293normalize_ip() { 294 # shellcheck disable=SC2034 295 local ip_bytes=() 296 ip_to_bytes ip_bytes "$1" || return 297 ip_bytes_to_str ip_bytes 298} 299 300network_init=1 301