#!/bin/bash
#
# so-nic-pin — pin physical NIC names by permanent MAC via classic by-MAC udev
#              rules, so a kernel upgrade can't renumber them.
#
# Security Onion binds its management and monitor interfaces BY NAME in pillar
# (host:mainint, sensor:mainint, and bond0 is built on a specific physical NIC).
# A kernel upgrade can change the kernel/systemd-udevd predictable-naming output
# and renumber those NICs (e.g. enp1s0 -> enp2s0), which breaks the grid: the
# pillar references a name that no longer exists and bond/bridge bring-up fails.
#
# This writes /etc/udev/rules.d/70-persistent-net.rules pinning each PHYSICAL NIC
# to its CURRENT name by its PERMANENT MAC, freezing the names across future kernel
# changes. It only writes the rules file; it does NOT live-trigger a rename (the
# rules apply on the next boot/kernel, and a live rename would be disruptive).
#
# Run-once: gated by the drop file /opt/so/state/nic_names_pinned. If the marker is
# present the script does nothing, so an admin can pre-create it to opt out. Invoked
# from the common state on every highstate; the marker keeps it a one-time setup.

NET_RULES_FILE="/etc/udev/rules.d/70-persistent-net.rules"
MARKER="/opt/so/state/nic_names_pinned"

log() { echo -e "[so-nic-pin] $*"; }

# Echo "<name> <permanent-mac>" for every PHYSICAL NIC. A physical NIC is backed by a
# real device (has device/driver), which excludes bond0/sobridge/docker0/veth*/lo whose
# MACs are dynamic and must never be pinned. The PERMANENT MAC is used (ethtool -P, with
# fallbacks), not the current one: an enslaved bond member's current MAC is rewritten to
# the bond's, so matching on it would be wrong/ambiguous.
physical_nics() {
    local path n mac
    for path in /sys/class/net/*; do
        n="${path##*/}"
        [ "$n" = "lo" ] && continue
        [ -e "${path}/device/driver" ] || continue          # real device only
        mac="$(ethtool -P "$n" 2>/dev/null | awk '/Permanent address/{print $NF}')"
        case "$mac" in ""|00:00:00:00:00:00) mac="$(cat "${path}/bonding_slave/perm_hwaddr" 2>/dev/null)" ;; esac
        case "$mac" in ""|00:00:00:00:00:00) mac="$(cat "${path}/address" 2>/dev/null)" ;; esac
        case "$mac" in ""|00:00:00:00:00:00) continue ;; esac
        echo "$n $mac"
    done
}

# Turn "<name> <mac>" lines on stdin into classic by-MAC persistent-net udev rules.
render_net_rules() {
    echo "# Generated by so-nic-pin: pin NIC names by MAC so kernel upgrades can't renumber them."
    echo "# Security Onion binds its management/monitor interfaces by name; do not hand-edit."
    local n mac
    while read -r n mac; do
        [ -n "$n" ] || continue
        printf 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="%s", NAME="%s"\n' \
            "$mac" "$n"
    done
}

[ "$(id -u)" -eq 0 ] || exit 0                   # salt runs us as root; bail quietly otherwise
[ -e "${MARKER}" ] && exit 0                      # run-once guard (mirrors the state's unless)

nics="$(physical_nics)"
if [ -z "${nics}" ]; then
    log "no physical NICs detected — nothing to pin (will retry on next highstate)"
    exit 0                                         # do NOT drop the marker; let it retry later
fi

log "pinning physical NICs by permanent MAC:"
echo "${nics}" | sed 's/^/    /'

[ -f "${NET_RULES_FILE}" ] && cp -f "${NET_RULES_FILE}" "${NET_RULES_FILE}.bak"
echo "${nics}" | render_net_rules > "${NET_RULES_FILE}" || {
    log "ERROR: failed to write ${NET_RULES_FILE}"
    exit 1
}

mkdir -p "$(dirname "${MARKER}")" && touch "${MARKER}"
log "wrote ${NET_RULES_FILE} ($(grep -c '^SUBSYSTEM' "${NET_RULES_FILE}") NIC(s) pinned); dropped ${MARKER}"
