#!/usr/bin/env bash
# Mock `ss` (iproute2). NEVER touches a real socket: the "socket table" is a
# plain text file in the test's own sandbox, and `-K` edits that file.
#
# $SONOMOS_TEST_SS_TABLE names the file: one socket per line, in exactly the
# shape `ss -tneH state established` prints —
#   <recv-q> <send-q> <local> <peer> [timer:(…)] [uid:<uid>] ino:<inode> sk:<id> cgroup:<path> <->
# (the real one, iproute2 7.2, leaves `uid:` out for uid 0, and the cgroup path
# may contain spaces). Absent, the table is empty.
#
# Three forms are understood, the three reset-uncovered issues:
#   ss -tneH state established dport = :443
#       prints EVERY row, on purpose looser than the real filter, so the
#       helper's own port/loopback/uid checks are what a test observes — or,
#       with $SONOMOS_TEST_SS_LIST=fail, fails the way an unreadable table does.
#   ss -K -tnH state established src <local> dst <peer> [dev = <dev>]
#       logs "KILL <local> <peer>[ dev <dev>]" to $SONOMOS_TEST_SS_LOG and
#       deletes that row — unless $SONOMOS_TEST_SS_KILL is `refuse` (the row
#       stays, as on a kernel without INET_DIAG_DESTROY) or `reconnect` (the
#       row is replaced by the same peer from a NEW local port, as a client
#       that reconnects at once would leave it).
#   ss -tnH state established src <local> dst <peer> [dev = <dev>]
#       prints the row if it is still there — or, with
#       $SONOMOS_TEST_SS_CHECK=fail, prints nothing and exits 1.
# Both match the way the real filter does. A row whose local end is spelled
# `addr%dev:port` (a socket bound to a device, SO_BINDTODEVICE) matches
# `src addr:port`, with or without `dev = dev`; and an address spelled WITH
# `%dev` in the filter is rejected, exit 1 and empty stdout, exactly as real
# `ss` rejects it ("an inet prefix is expected").
# Plus `ss -ltnH`, which prints $SONOMOS_TEST_SS_LISTEN (the shim listener
# check in `redirect_on`'s direct path).
#
# Anything else is a hard-fail tripwire.
table="${SONOMOS_TEST_SS_TABLE:-/dev/null}"
log() { [ -n "${SONOMOS_TEST_SS_LOG:-}" ] && printf '%s\n' "$*" >> "$SONOMOS_TEST_SS_LOG"; return 0; }

case "$*" in
  "-ltnH")
    [ -n "${SONOMOS_TEST_SS_LISTEN:-}" ] && printf '%s\n' "$SONOMOS_TEST_SS_LISTEN"
    exit 0 ;;
  "-tneH state established dport = :443")
    log "LIST"
    # `fail`: ss could not read the table at all (netlink refused, say).
    [ "${SONOMOS_TEST_SS_LIST:-}" = fail ] && { echo "Cannot open netlink socket" >&2; exit 1; }
    [ -f "$table" ] && cat "$table"
    exit 0 ;;
esac

kill=0
if [ "${1:-}" = "-K" ]; then kill=1; shift; fi
if [ "${1:-}" = "-tnH" ] && [ "${2:-}" = "state" ] && [ "${3:-}" = "established" ] \
   && [ "${4:-}" = "src" ] && [ "${6:-}" = "dst" ] \
   && { [ "$#" -eq 7 ] || { [ "$#" -eq 10 ] && [ "$8" = "dev" ] && [ "$9" = "=" ]; }; }; then
  src="$5"; dst="$7"; dev="${10:-}"
  case "$src $dst" in
    *%*)
      log "REJECT $src $dst"
      echo "Error: an inet prefix is expected rather than \"${src%:*}\"." >&2
      echo "Cannot parse dst/src address." >&2
      exit 1 ;;
  esac
  # hit(): does this row match the filter? Its local end loses any `%dev`
  # before the comparison, and the device is compared only when asked for.
  match='
    function hit(   l, p, port, rdev) {
      l = $3; rdev = ""
      if ((p = index(l, "%")) > 0) {
        port = l; sub(/^.*:/, "", port)
        rdev = substr(l, p + 1); sub(/:[0-9]+$/, "", rdev)
        l = substr(l, 1, p - 1) ":" port
      }
      return l == s && $4 == d && (dv == "" || rdev == dv)
    }'
  if [ "$kill" = 0 ]; then
    log "CHECK $src $dst${dev:+ dev $dev}"
    [ "${SONOMOS_TEST_SS_CHECK:-}" = fail ] && { echo "Cannot open netlink socket" >&2; exit 1; }
    [ -f "$table" ] && awk -v s="$src" -v d="$dst" -v dv="$dev" "$match"' hit()' "$table"
    exit 0
  fi
  log "KILL $src $dst${dev:+ dev $dev}"
  [ -f "$table" ] || exit 0
  case "${SONOMOS_TEST_SS_KILL:-}" in
    refuse) exit 0 ;;
    reconnect)
      awk -v s="$src" -v d="$dst" -v dv="$dev" "$match"'
        hit() { sub(/:[0-9]+$/, ":65000", $3); print; next }
        { print }' "$table" > "$table.new" ;;
    *)
      awk -v s="$src" -v d="$dst" -v dv="$dev" "$match"' !hit()' "$table" > "$table.new" ;;
  esac
  mv -f "$table.new" "$table"
  exit 0
fi

echo "MOCK-ss: refusing unexpected invocation: $*" >&2
exit 97
