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