xref: /openbmc/openbmc/meta-google/recipes-google/networking/network-sh/lib.sh (revision f442540acb1cd478286a2c1899e4f3528c817684)
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