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 ${gbmc_br_gw_src_lib-} ]] && return
17
18# shellcheck source=meta-google/recipes-google/networking/network-sh/lib.sh
19source /usr/share/network/lib.sh || exit
20# shellcheck source=meta-google/recipes-google/networking/gbmc-net-common/gbmc-net-lib.sh
21source /usr/share/gbmc-net-lib.sh || exit
22
23declare -A gbmc_br_gw_src_ips=()
24declare -A gbmc_br_gw_src_routes=()
25gbmc_br_gw_defgw=
26
27gbmc_br_set_router() {
28  local defgw=
29  local route
30  for route in "${!gbmc_br_gw_src_routes[@]}"; do
31    if [[ $route != *' dev gbmcbr '* ]]; then
32      defgw=1
33      break
34    fi
35  done
36  # Make becoming a router sticky, if we ever have a default route we are
37  # always treated as a router. Otherwise, we end up reloading unnecessarily
38  # a number of times. The reload causes the network configuration to be
39  # reappplied with packet drops for a short amount of time.
40  [[ -z $defgw ]] && return
41  [[ $defgw == "$gbmc_br_gw_defgw" ]] && return
42  gbmc_br_gw_defgw="$defgw"
43
44  local files=(/run/systemd/network/{00,}-bmc-gbmcbr.network.d/50-defgw.conf)
45  if [[ -n $defgw ]]; then
46    local file
47    for file in "${files[@]}"; do
48      mkdir -p "$(dirname "$file")"
49      printf '[IPv6SendRA]\nRouterLifetimeSec=120\n' >"$file"
50    done
51  else
52    rm -f "${files[@]}"
53  fi
54
55  # shellcheck disable=SC2119
56  gbmc_net_networkd_reload
57}
58
59gbmc_br_gw_src_update() {
60  # Pick the shortest address, we always want to use the most root level
61  # The order of preference looks roughly like
62  #   1. Root /64 address (2620:15c:2c3:aaae::/64)
63  #      This is generally used by the OOB RJ45 port and is our primary preference
64  #   2. BMC subordonate root (2620:15c:2c3:aaae:fd01::/80)
65  #      From the NIC over NCSI with the /64 shared with the CN
66  #   3. BMC stateless (2620:15c:2c3:aaae:fd00:3c8d:20dc:263e/80)
67  #      From the NIC, but derived from the MAC and typically never used
68  #
69  local new_src=
70  local new_len=16
71  local ip
72  for ip in "${!gbmc_br_gw_src_ips[@]}"; do
73    local ip_len="${gbmc_br_gw_src_ips["$ip"]}"
74    if (( ip_len < new_len )); then
75      new_src="$ip"
76      new_len="$ip_len"
77    fi
78  done
79  (( new_len >= 16 )) && return
80
81  local route
82  for route in "${!gbmc_br_gw_src_routes[@]}"; do
83    [[ $route != *" src $new_src "* ]] || continue
84    echo "gBMC Bridge Updating GW source [$new_src]: $route" >&2
85    # shellcheck disable=SC2086
86    ip route change $route src "$new_src" && \
87      unset 'gbmc_br_gw_src_routes[$route]'
88  done
89}
90
91gbmc_br_gw_src_hook() {
92  # We only want to match default gateway routes that are dynamic
93  # (have an expiration time). These will be updated with our preferred
94  # source.
95  # shellcheck disable=SC2154
96  if [[ $change == route && $route == 'default '*':'* ]]; then
97    if [[ $route =~ ^(.*)( +expires +[^ ]+)(.*)$ ]]; then
98      route="${BASH_REMATCH[1]}${BASH_REMATCH[3]}"
99    fi
100    if [[ $action == add && -z ${gbmc_br_gw_src_routes["$route"]} ]]; then
101      gbmc_br_gw_src_routes["$route"]=1
102      gbmc_br_gw_src_update
103      gbmc_br_set_router
104    elif [[ $action == del && -n "${gbmc_br_gw_src_routes["$route"]}" ]]; then
105      unset 'gbmc_br_gw_src_routes[$route]'
106      gbmc_br_gw_src_update
107      gbmc_br_set_router
108    fi
109  # Match only global IP addresses on the bridge that are non-ULA addresses.
110  # So 2002:af4:3480:2248:fd00:6345:3069:9186 would be
111  # matched as the preferred source IP for outoging traffic.
112  elif [[ $change == addr && $intf == gbmcbr && $scope == global ]] &&
113       [[ $fam == inet6 && $flags != *tentative* ]]; then
114    local ip_bytes=()
115    if ! ip_to_bytes ip_bytes "$ip"; then
116      echo "gBMC Bridge Ensure RA Invalid IP: $ip" >&2
117      return 1
118    fi
119    # Ignore ULAs
120    if (( (ip_bytes[0] & 0xfe) == 0xfc )); then
121      return 0
122    fi
123    if [[ $action == add ]]; then
124      local i=0
125      local non_zero=0
126      for (( i=0; i<16; ++i )); do
127        if (( ip_bytes[i] != 0 )); then
128          non_zero="$i"
129        fi
130      done
131      gbmc_br_gw_src_ips["$ip"]="$non_zero"
132    elif [[ $action == del ]]; then
133      unset 'gbmc_br_gw_src_ips[$ip]'
134    fi
135    gbmc_br_gw_src_update
136  fi
137}
138
139GBMC_IP_MONITOR_HOOKS+=(gbmc_br_gw_src_hook)
140
141gbmc_br_gw_src_lib=1
142