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