ncsid: Support parsing unsolicited RA announcements

We depend on this for router's whose MACs change over the lifetime of
the BMC and need updates even when we can't send neighbor solicitations
and receive advertisements.

Change-Id: Id7eb8ee8d44aea597a63276acc698f8fee9059b2
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/subprojects/ncsid/src/meson.build b/subprojects/ncsid/src/meson.build
index 87805d7..f3e708c 100644
--- a/subprojects/ncsid/src/meson.build
+++ b/subprojects/ncsid/src/meson.build
@@ -86,6 +86,11 @@
   install_dir: get_option('libexecdir'))
 
 install_data(
+  'update_ra_neighbor.sh',
+  install_mode: 'rwxr-xr-x',
+  install_dir: get_option('libexecdir'))
+
+install_data(
   'update_static_neighbors.sh',
   install_mode: 'rwxr-xr-x',
   install_dir: get_option('libexecdir'))
@@ -103,6 +108,13 @@
   install_dir: systemunitdir)
 
 configure_file(
+  configuration: {'BIN': libexecdir / 'update_ra_neighbor.sh'},
+  input: 'update-ra-neighbor@.service.in',
+  output: 'update-ra-neighbor@.service',
+  install_mode: 'rw-r--r--',
+  install_dir: systemunitdir)
+
+configure_file(
   configuration: {'BIN': libexecdir / 'update_static_neighbors.sh'},
   input: 'update-static-neighbors@.service.in',
   output: 'update-static-neighbors@.service',
@@ -128,6 +140,7 @@
 install_data(
   'nic-hostful@.target',
   'nic-hostless@.target',
+  'update-ra-neighbor@.timer',
   'update-static-neighbors@.timer',
   install_mode: 'rw-r--r--',
   install_dir: systemunitdir)
diff --git a/subprojects/ncsid/src/ncsid_lib.sh b/subprojects/ncsid/src/ncsid_lib.sh
index d96024b..e7d3fb0 100644
--- a/subprojects/ncsid/src/ncsid_lib.sh
+++ b/subprojects/ncsid/src/ncsid_lib.sh
@@ -265,6 +265,34 @@
   GetProperties "$service" "$object" 'xyz.openbmc_project.Network.IP'
 }
 
+# Returns the Gateway address for the interface and type
+GetGateways() {
+  local service="$1"
+  local netdev="$2"
+
+  # We fetch both the system properties and the netdev specific properties
+  # as OpenBMC is in the process of transitioning these to the netdev object
+  # but the migration is not yet complete.
+  {
+    GetProperties "$service" '/xyz/openbmc_project/network/config' \
+      'xyz.openbmc_project.Network.SystemConfiguration'
+    GetProperties "$service" "$(EthObjRoot "$netdev")" \
+      'xyz.openbmc_project.Network.EthernetInterface'
+  } | jq -s '
+      . | map(
+        if .DefaultGateway != "" then
+          {DefaultGateway: .DefaultGateway}
+        else
+          {}
+        end +
+        if .DefaultGateway6 != "" then
+          {DefaultGateway6: .DefaultGateway6}
+        else
+          {}
+        end
+      ) | {DefaultGateway: "", DefaultGateway6: ""} + add'
+}
+
 # Adds a static IP to the system network daemon
 AddIP() {
   local service="$1"
@@ -396,3 +424,30 @@
     AddNeighbor "$service" "$netdev" "$ip" "$mac" || return $?
   fi
 }
+
+# Determines the ip and mac of the IPv6 router
+DiscoverRouter6() {
+  local netdev="$1"
+  local retries="$2"
+  local timeout="$3"
+  local router="${4-}"
+
+  local output
+  local st=0
+  local args=(-1 -w "$timeout" -n $router "$netdev")
+  if (( retries < 0 )); then
+    args+=(-d)
+  else
+    args+=(-r "$retries")
+  fi
+  output="$(RunInterruptible rdisc6 "${args[@]}")" || st=$?
+  if (( st != 0 )); then
+    echo "rdisc6 failed with: " >&2
+    echo "$output" >&2
+    return $st
+  fi
+
+  local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
+  local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
+  printf '{"router_ip":"%s","router_mac":"%s"}\n' "$ip" "$mac"
+}
diff --git a/subprojects/ncsid/src/ncsid_udhcpc6.script b/subprojects/ncsid/src/ncsid_udhcpc6.script
index c5a3d7f..508f2b4 100644
--- a/subprojects/ncsid/src/ncsid_udhcpc6.script
+++ b/subprojects/ncsid/src/ncsid_udhcpc6.script
@@ -1,23 +1,6 @@
 #!/bin/bash
 source "$(dirname "${BASH_SOURCE[0]}")"/ncsid_lib.sh
 
-DiscoverRouter6() {
-  local netdev="$1"
-
-  local output
-  local st=0
-  output="$(RunInterruptible rdisc6 -1 -r 5 -w 1000 -n "$netdev")" || st=$?
-  if (( st != 0 )); then
-    echo "rdisc6 failed with: " >&2
-    echo "$output" >&2
-    return $st
-  fi
-
-  local ip="$(echo "$output" | grep 'from' | awk '{print $2}')"
-  local mac="$(echo "$output" | grep 'Source link-layer' | ParseMACFromLine)"
-  printf '{"router_ip":"%s","router_mac":"%s"}\n' "$ip" "$mac"
-}
-
 HandleDHCP6() {
   local op="$1"
 
@@ -26,7 +9,7 @@
     echo "IP: $ipv6/128" >&2
 
     local disc
-    if ! disc="$(DiscoverRouter6 "$interface")"; then
+    if ! disc="$(DiscoverRouter6 "$interface" 5 1000)"; then
       echo "Failed to discover router" >&2
       return 1
     fi
diff --git a/subprojects/ncsid/src/nic-hostful@.target b/subprojects/ncsid/src/nic-hostful@.target
index 0d2d231..e3234b8 100644
--- a/subprojects/ncsid/src/nic-hostful@.target
+++ b/subprojects/ncsid/src/nic-hostful@.target
@@ -3,3 +3,5 @@
 Conflicts=nic-hostless@%i.target
 BindsTo=ncsid@%i.service
 After=ncsid@%i.service
+Wants=update-ra-neighbor@%i.service
+Wants=update-ra-neighbor@%i.timer
diff --git a/subprojects/ncsid/src/nic-hostless@.target b/subprojects/ncsid/src/nic-hostless@.target
index 3d9b496..1290316 100644
--- a/subprojects/ncsid/src/nic-hostless@.target
+++ b/subprojects/ncsid/src/nic-hostless@.target
@@ -7,3 +7,5 @@
 Wants=dhcp6@%i.service
 Wants=update-static-neighbors@%i.service
 Wants=update-static-neighbors@%i.timer
+Wants=update-ra-neighbor@%i.service
+Wants=update-ra-neighbor@%i.timer
diff --git a/subprojects/ncsid/src/update-ra-neighbor@.service.in b/subprojects/ncsid/src/update-ra-neighbor@.service.in
new file mode 100644
index 0000000..d946939
--- /dev/null
+++ b/subprojects/ncsid/src/update-ra-neighbor@.service.in
@@ -0,0 +1,15 @@
+[Unit]
+Description=RA Neighbor Updater
+Wants=mapper-wait@-xyz-openbmc_project-network-%i.service
+After=mapper-wait@-xyz-openbmc_project-network-%i.service
+BindsTo=ncsid@%i.service
+After=ncsid@%i.service
+StartLimitIntervalSec=1min
+StartLimitBurst=5
+
+[Service]
+KillMode=mixed
+Restart=on-failure
+ExecStart=@@BIN@ update-ra-neighbor %I
+SyslogIdentifier=update-ra-neighbor@%I
+SuccessExitStatus=10
diff --git a/subprojects/ncsid/src/update-ra-neighbor@.timer b/subprojects/ncsid/src/update-ra-neighbor@.timer
new file mode 100644
index 0000000..f74461a
--- /dev/null
+++ b/subprojects/ncsid/src/update-ra-neighbor@.timer
@@ -0,0 +1,7 @@
+[Unit]
+Description=Retry neighbor update periodically
+BindsTo=ncsid@%i.service
+After=ncsid@%i.service
+
+[Timer]
+OnUnitInactiveSec=1min
diff --git a/subprojects/ncsid/src/update_ra_neighbor.sh b/subprojects/ncsid/src/update_ra_neighbor.sh
new file mode 100644
index 0000000..94d19e1
--- /dev/null
+++ b/subprojects/ncsid/src/update_ra_neighbor.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+source "$(dirname "${BASH_SOURCE[0]}")"/ncsid_lib.sh
+
+UpdateRA() {
+  local netdev="$1"
+
+  local service='xyz.openbmc_project.Network'
+  local gateways
+  if ! gateways="$(GetGateways "$service" "$netdev")"; then
+    echo "Failed to look up gateways" >&2
+    return 1
+  fi
+  local vars
+  vars="$(echo "$gateways" | JSONToVars)" || return
+  eval "$vars" || return
+
+  echo "GW($netdev): ${DefaultGateway6:-(none)}" >&2
+  if [ -z "$DefaultGateway6" ]; then
+    return 0
+  fi
+
+  local disc
+  if ! disc="$(DiscoverRouter6 "$netdev" -1 360000 "$DefaultGateway6")"; then
+    echo "Failed to discover router" >&2
+    return 1
+  fi
+  local vars
+  vars="$(echo "$disc" | JSONToVars)" || return
+  eval "$vars" || return
+  echo "GW($netdev) MAC: $router_mac" >&2
+
+  SuppressTerm
+  local rc=0
+  UpdateNeighbor "$service" "$netdev" "$router_ip" "$router_mac" || rc=$?
+  UnsuppressTerm
+  return $rc
+}
+
+Main() {
+  set -o nounset
+  set -o errexit
+  set -o pipefail
+
+  InitTerm
+  UpdateRA "$@"
+}
+
+return 0 2>/dev/null
+Main "$@"