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# shellcheck source=meta-google/recipes-google/networking/network-sh/lib.sh
17source /usr/share/network/lib.sh || exit
18# shellcheck source=meta-google/recipes-google/networking/gbmc-net-common/gbmc-net-lib.sh
19source /usr/share/gbmc-net-lib.sh || exit
20
21: "${RA_IF:?No RA interface set}"
22: "${IP_OFFSET=?1}"
23: "${ROUTE_METRIC:?No Metric set}"
24
25# We would prefer empty string but it's easier for associative array handling
26# to use invalid
27old_rtr=invalid
28old_mac=invalid
29old_pfx=invalid
30old_fqdn=invalid
31
32default_update_rtr() {
33  local rtr="$1"
34  local mac="$2"
35
36  if ip addr show | grep -q "^[ ]*inet6 $rtr/"; then
37    echo "Router is ourself, ignoring" >&2
38    return 0
39  fi
40
41  # In case we don't have a base network file, make one
42  # this is intentionally 00- as it will not preceed /etc/systemd/network/00-*
43  # or /lib/systemd/network/-* files.
44  local file=/run/systemd/network/00-bmc-$RA_IF.network
45  printf '[Match]\nName=%s\n[Network]\nDHCP=false\nIPv6AcceptRA=false\nLinkLocalAddressing=yes' \
46    "$RA_IF" >"$file"
47
48  # Override any existing gateway information within files
49  # Make sure we cover `00-*` and `-*` files
50  for file in /run/systemd/network/{00,}-bmc-$RA_IF.network; do
51    mkdir -p "$file.d"
52    printf '[Route]\nGateway=%s\nGatewayOnLink=true\nMetric=%d\n[Neighbor]\nMACAddress=%s\nAddress=%s' \
53      "$rtr" "$ROUTE_METRIC" "$mac" "$rtr" >"$file.d"/10-gateway.conf
54  done
55
56  # Don't force networkd to reload as this can break phosphor-networkd
57  # Fall back to reload only if ip link commands fail
58  (ip -6 route replace default via "$rtr" onlink dev "$RA_IF" metric "$ROUTE_METRIC" && \
59    ip -6 neigh replace "$rtr" dev "$RA_IF" lladdr "$mac") || \
60    gbmc_net_networkd_reload "$RA_IF" || true
61
62  echo "Set router $rtr on $RA_IF" >&2
63}
64
65default_update_fqdn() {
66  local fqdn="$1"
67  [ -z "$fqdn" ] && return
68  hostnamectl set-hostname "$fqdn" || true
69  echo "Set hostname $fqdn on $RA_IF" >&2
70}
71
72retries=1
73min_w=10
74declare -A rtrs
75rtrs=()
76while true; do
77  # shellcheck disable=SC2206
78  data=(${rtrs["${old_rtr}"]-})
79  curr_dl="${data[1]-$(( min_w + SECONDS ))}"
80  args=(-m "$RA_IF" -w $(( (curr_dl - SECONDS) * 1000 )))
81  if (( retries > 0 )); then
82    args+=(-r "$retries")
83  else
84    args+=(-d)
85  fi
86  while read -r line; do
87    # `script` terminates all lines with a CRLF, remove it
88    line="${line:0:-1}"
89    # shellcheck disable=SC2026
90    if [ -z "$line" ]; then
91      lifetime=-1
92      mac=
93      hextet=
94      pfx=
95      host=
96      domain=
97    elif [[ "$line" =~ ^Router' 'lifetime' '*:' '*([0-9]*) ]]; then
98      lifetime="${BASH_REMATCH[1]}"
99    elif [[ "$line" =~ ^Source' 'link-layer' 'address' '*:' '*([a-fA-F0-9:]*)$ ]]; then
100      mac="${BASH_REMATCH[1]}"
101    elif [[ "$line" =~ ^Prefix' '*:' '*(.*)/([0-9]+)$ ]]; then
102      t_pfx="${BASH_REMATCH[1]}"
103      t_pfx_len="${BASH_REMATCH[2]}"
104      ip_to_bytes t_pfx_b "$t_pfx" || continue
105      (( (t_pfx_len == 76 || t_pfx_len == 80) && (t_pfx_b[8] & 0xfd) == 0xfd )) || continue
106      (( t_pfx_b[9] &= 0xf0 ))
107      (( t_pfx_b[9] |= IP_OFFSET ))
108      hextet="fd$(printf '%02x' "${t_pfx_b[9]}")"
109      pfx="$(ip_bytes_to_str t_pfx_b)"
110    elif [[ "$line" =~ ^'DNS search list'' '*:' '*([^.]+)(.*[.]google[.]com)' '*$ ]]; then
111      # Ideally, we use PCRE and with lookahead and can do this in a single regex
112      #   ^([a-zA-Z0-9-]+(?=-n[a-fA-F0-9]{1,4})|[a-zA-Z0-9-]+(?!-n[a-fA-F0-9]{1,4}))[^.]*[.]((?:[a-zA-Z0-9]*[.])*google[.]com)$
113      # Instead we do multiple steps to extract the needed info
114      host="${BASH_REMATCH[1]}"
115      domain="${BASH_REMATCH[2]#.}"
116      if [[ "$host" =~ (-n[a-fA-F0-9]{1,4})$ ]]; then
117        host="${host%"${BASH_REMATCH[1]}"}"
118      fi
119    elif [[ "$line" =~ ^from' '(.*)$ ]]; then
120      rtr="${BASH_REMATCH[1]}"
121      # Only valid default routers can be considered, 0 lifetime implies
122      # a non-default router
123      (( lifetime > 0 )) || continue
124
125      dl=$((lifetime + SECONDS))
126      fqdn=
127      if [[ -n $host && -n $hextet && -n $domain ]]; then
128        fqdn="$host-n$hextet.$domain"
129      fi
130      rtrs["$rtr"]="$mac $dl $pfx $fqdn"
131      # We have some notoriously noisy lab environments with many routers being broadcast
132      # We always prefer "fe80::1" in prod and labs for routing, so prefer that gateway.
133      # We also want to take the first router we find to speed up acquisition on boot.
134      if [[ "$rtr" = "fe80::1" || "$old_rtr" = "invalid" ]]; then
135        if [[ "$rtr" != "$old_rtr" && "$mac" != "$old_mac" ]]; then
136          echo "Got defgw $rtr at $mac on $RA_IF" >&2
137          update_rtr "$rtr" "$mac" || true
138          retries=-1
139          old_mac="$mac"
140          old_rtr="$rtr"
141        fi
142      fi
143      # Only update router properties if we use this router
144      [[ "$rtr" == "$old_rtr" ]] || continue
145      if [[ $pfx != "$old_pfx" ]]; then
146        echo "Got PFX $pfx from $rtr on $RA_IF" >&2
147        old_pfx="$pfx"
148        update_pfx "$pfx" || true
149      fi
150      if [[ $fqdn != "$old_fqdn" ]]; then
151        echo "Got FQDN $fqdn from $rtr on $RA_IF" >&2
152        old_fqdn="$fqdn"
153        update_fqdn "$fqdn" || true
154      fi
155    fi
156  done < <(exec script -q -c "rdisc6 ${args[*]}" /dev/null 2>/dev/null)
157  # Purge any expired routers
158  for rtr in "${!rtrs[@]}"; do
159    # shellcheck disable=SC2206
160    data=(${rtrs["$rtr"]})
161    dl=${data[1]}
162    if (( dl <= SECONDS )); then
163      unset "rtrs[$rtr]"
164    fi
165  done
166  # Consider changing the gateway if the old one doesn't send RAs for the entire period
167  # This ensures we don't flip flop between multiple defaults if they exist.
168  if [[ "$old_rtr" != "invalid" && -z "${rtrs["$old_rtr"]-}" ]]; then
169    echo "Old router $old_rtr disappeared" >&2
170    old_rtr=invalid
171    for rtr in "${!rtrs[@]}"; do
172      # shellcheck disable=SC2206
173      data=(${rtrs["$rtr"]})
174      mac=${data[0]}
175      dl=${data[1]}
176      pfx=${data[2]}
177      fqdn=${data[3]}
178      update_rtr "$rtr" "$mac" || true
179      update_pfx "$pfx" || true
180      update_fqdn "$fqdn" || true
181      break
182    done
183  fi
184
185  # If rdisc6 exits early we still want to wait for the deadline before retrying
186  (( timeout = curr_dl - SECONDS ))
187  sleep $(( timeout < 0 ? 0 : timeout ))
188done
189