Some checks failed
Run Check Script / check (pull_request) Failing after 40s
On multi-NIC FreeBSD/OPNsense boxes (Wize 5070 and similar), PCIe enumeration order shuffles igc0/igc1/... across reboots. OPNsense binds wan/lan assignments to interface names, so a shuffle silently re-points them at the wrong physical ports and breaks firewall rules. Validated fix from OPNsense forum #27023 (endorsed by franco): the upstream `ethname` rc.d script (MIT, © Eric Borisch 2016–2019, frozen at v2.0.1) does a two-stage rename in early boot — before `netif` — mapping MACs to fixed interface names. Vendor the 280-line script inline rather than `pkg install ethname`. `pkg install` on a fresh ISO often fails because the firmware lags the live pkg repo, and the firmware-upgrade reboot is precisely the boot we need to defend against. Vendoring sidesteps the chicken-and-egg. Adds: harmony/data/opnsense/ethname.sh vendored upstream script (verbatim) harmony/data/opnsense/ethname.LICENSE preserves MIT terms bootstrap.rs: ETHNAME_SCRIPT (const, include_str!) DEFAULT_PHYSICAL_DRIVER_PREFIXES (const) list_physical_nics_via_ssh / read_ethname_mac_set_via_ssh / install_ethname_via_ssh (pub SSH helpers) pin_nic_names module: pin_nic_names_step — the shared one-shot logic OPNsensePinNicNamesScore — Score<OPNsenseBootstrapTopology> for ad-hoc re-pinning / standalone use OPNsenseBootstrapScore composes pin_nic_names_step internally as a mandatory step between the web UI dance and API key mint — every firewall bootstrapped through harmony gets pinned NIC names automatically, no caller code change required. Idempotent: re-running on a firewall whose MAC set already matches /etc/rc.conf.d/ethname is a NOOP. The existence probe for the config file is wrapped in `sh -c '...'` because OPNsense's root login shell is /bin/csh (tcsh); bare Bourne if/then/else fails there. Simple `&&` chains (the pattern in the other SSH helpers) work in both shells. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
281 lines
7.8 KiB
Bash
281 lines
7.8 KiB
Bash
#!/bin/sh
|
|
#
|
|
# * Copyright (c) 2016-2019 Eric Borisch <eborisch@gmail.com>
|
|
# * All rights reserved.
|
|
#
|
|
# Self-contained rc.d script for re-naming devices based on their MAC address.
|
|
# Renaming is performed before interface bring-up -- netif -- so all
|
|
# configurations of the devices can be done with the new names.
|
|
#
|
|
# USAGE:
|
|
# 1) Add the following to rc.conf:
|
|
# ethname_enable="YES"
|
|
# ethname_external_mac="aa:bb:cc:dd:ee:00"
|
|
# ethname_private_mac="aa:bb:cc:dd:ee:01"
|
|
# 1a) You can optionally restrict handling to a set of defined names with:
|
|
# ethname_names="external private"
|
|
# otherwise all defined ethname_*_mac="" values are used
|
|
# 2) Make sure any interfaces you want to rename have their drivers loaded or
|
|
# compiled in. If ue0 is on axe0, for example, add 'if_load_axe="YES"' to
|
|
# /boot/loader.conf. See the man page for your device (eg 'man axe') for
|
|
# particulars.
|
|
# 3) That's it. Use ifconfig_<name>="" settings with the new names.
|
|
#
|
|
# All other devices are untouched.
|
|
#
|
|
# Optional rc.conf settings:
|
|
# ethname_timeout : Maximum wait time for devices to appear. [default=30]
|
|
#
|
|
# PROVIDE: ethname
|
|
# REQUIRE: FILESYSTEMS
|
|
# BEFORE: netif
|
|
# KEYWORD: nojail
|
|
|
|
# ethname version 2.0
|
|
|
|
. /etc/rc.subr
|
|
|
|
name=ethname
|
|
rcvar=ethname_enable
|
|
extra_commands="check"
|
|
check_cmd="en_check"
|
|
|
|
start_cmd="${name}_start"
|
|
stop_cmd=":"
|
|
|
|
load_rc_config ${name}
|
|
: ${ethname_names:=""}
|
|
: ${ethname_enable:=no}
|
|
: ${ethname_timeout:="30"}
|
|
|
|
en_str=""
|
|
|
|
# Will fill with mac interface [mac interface] ...]
|
|
en_map=""
|
|
|
|
# Will fill with original device names that match a managed mac address.
|
|
en_orig=""
|
|
|
|
# Total wait timeout; won't wait n*timeout for n devices, just timeout
|
|
en_waited=0
|
|
|
|
known_mac()
|
|
{
|
|
echo "${en_map}" | grep -qi "$1"
|
|
}
|
|
|
|
to_lower()
|
|
{
|
|
echo "$*" | tr "[:upper:]" "[:lower:]"
|
|
}
|
|
|
|
|
|
kv_lookup()
|
|
{
|
|
# Called with $1=K, the key we want to find the value for, and $2:$3
|
|
# $4:$5 ... forming pairs of key:value mappings
|
|
local _K _key _value
|
|
|
|
_K=$(to_lower "$1")
|
|
[ -z "${_K}" ] && err 1 "Called kv_lookup() with missing args."
|
|
shift
|
|
while [ $# -ge 2 ]; do
|
|
_key=$(to_lower "$1")
|
|
_value=$2
|
|
shift 2
|
|
# Only supports non-zero-length keys/values
|
|
[ -z "${_key}" -o -z "${_value}" ] && err 1 "Zero length values passed?"
|
|
[ "${_key}" == "${_K}" ] && echo "${_value}" && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
good_mac() {
|
|
echo "$1" | egrep -qi '^([0-9a-z]{2}:){5}[0-9a-z]{2}$' || \
|
|
err 1 "Invalid MAC address defined: [$1]"
|
|
return 0
|
|
}
|
|
|
|
good_devname() {
|
|
echo "$1" | egrep -qi '^[a-z][a-z0-9_]+$' || \
|
|
err 1 "Invalid device name defined: [$1]"
|
|
return 0
|
|
}
|
|
|
|
breakout_map () {
|
|
# This takes a single ethname_map variable (old interface) and breaks it
|
|
# into the new interface (ethname_names and ethname_NAME_mac vars.)
|
|
local _mac _name
|
|
while [ $# -gt 0 ]; do
|
|
_mac=$1
|
|
_name=$2
|
|
good_mac "${_mac}"
|
|
good_devname "${_name}"
|
|
shift 2
|
|
# Params checked for validity above
|
|
eval ethname_${_name}_mac="${_mac}"
|
|
ethname_names="${ethname_names} ${_name}"
|
|
done
|
|
}
|
|
|
|
en_prep()
|
|
{
|
|
local _mac _name _dev _found
|
|
local _compat=0
|
|
|
|
if [ -z "${ethname_names}" ]; then
|
|
# Compatibility code
|
|
if [ ! -z "${ethname_map}" -a ! -z "${ethname_devices}" ]; then
|
|
ethname_names=""
|
|
warn "ethname: Using old interface. Please see documentation."
|
|
breakout_map ${ethname_map}
|
|
_compat=1
|
|
else
|
|
# Detect set ethname_*_mac names
|
|
ethname_names=$(set | sed -En '/^ethname_([^=]+)_mac=.*/s//\1/p')
|
|
fi
|
|
fi
|
|
|
|
# Transforms set of ethname_NAME_mac="" values into en_map="MAC NAME ..."
|
|
# and en_orig="EXISTINGDEV ..."; a map of desired MAC:name mappings
|
|
# and the devices with those MACs, respectively.
|
|
|
|
for _name in ${ethname_names}; do
|
|
# Make sure ${_name} is good before eval call
|
|
good_devname "${_name}"
|
|
eval _mac=\$ethname_${_name}_mac
|
|
|
|
[ -z "${_mac}" -a ${_compat} -eq 0 ] && \
|
|
warn "ethname_${_name}_mac is not set in rc.conf!" && continue
|
|
|
|
good_mac "${_mac}"
|
|
|
|
# Enable ctrl-c for wait loop
|
|
trap break SIGINT
|
|
|
|
_found=0
|
|
while [ ${en_waited} -lt ${ethname_timeout} ]; do
|
|
for _dev in $(ifconfig -l ether); do
|
|
if ifconfig ${_dev} | grep -qi "${_mac}"; then
|
|
en_map="${en_map} ${_mac} ${_name}"
|
|
en_orig="${en_orig} ${_dev}"
|
|
_found=1
|
|
break
|
|
fi
|
|
done
|
|
[ ${_found} -eq 1 ] && break
|
|
sleep 1
|
|
warn "Waiting for a device with MAC [${_mac}] to appear..."
|
|
en_waited=$((en_waited + 1))
|
|
done
|
|
|
|
trap - SIGINT
|
|
|
|
[ ${_found} -eq 0 ] && \
|
|
warn "Unable to locate device to rename [${_name}]!"
|
|
done
|
|
}
|
|
|
|
en_check() {
|
|
local _mac _name _orig
|
|
local _n=1
|
|
en_prep
|
|
# Piping into a while loop, but we don't need any results from this loop to
|
|
# be visible in this shell, so it's not an issue.
|
|
echo "${en_map}" | xargs -n 2 echo | while read _mac _name; do
|
|
_orig=$(echo "${en_orig}" | awk "{print \$${_n}}")
|
|
if [ "${_orig}" = "${_name}" ]; then
|
|
printf "Device with MAC [%s] already named '%s'\n" \
|
|
"${_mac}" "${_name}"
|
|
else
|
|
printf "Will rename [%s] to [%s] with MAC [%s]\n" \
|
|
"${_orig}" "${_name}" "${_mac}"
|
|
fi
|
|
_n=$((_n + 1))
|
|
done
|
|
}
|
|
|
|
fix_name()
|
|
{
|
|
# Can be called with or without a second argument (which is used as the new
|
|
# name if provided.) If only one argument, lookup desired name in map.
|
|
dev=$1
|
|
name=$2
|
|
|
|
# Make sure the device exists as an ifconfig device
|
|
if ! ifconfig -l ether | grep -q "${dev}"; then
|
|
en_str="could not find device."
|
|
return 1
|
|
fi
|
|
|
|
# Grab MAC address
|
|
mac=$(ifconfig ${dev} | awk '/ether/{print tolower($2)}')
|
|
|
|
if [ ${#mac} -eq 0 ]; then
|
|
en_str="unable to get MAC address"
|
|
return 1
|
|
fi
|
|
|
|
# Make sure the MAC for this device is in our rename table.
|
|
if ! known_mac "${mac}"; then
|
|
en_str="no maching MAC in ethname_<NAME>_mac params."
|
|
return 1
|
|
fi
|
|
|
|
# Find name from MAC -> dev_name table in map
|
|
dname=$(kv_lookup ${mac} ${en_map})
|
|
if [ "${dname}" == "${dev}" ]; then
|
|
en_str="already has desired name."
|
|
return 1
|
|
fi
|
|
|
|
# Use name from MAC -> dev_name table in map if $2 was empty
|
|
: ${name:=${dname}}
|
|
|
|
# We have everything we need. Now actual rename of the device.
|
|
if ! ifconfig ${dev} name ${name} > /dev/null ; then
|
|
en_str="return code: $?"
|
|
return 2
|
|
fi
|
|
}
|
|
|
|
ethname_start()
|
|
{
|
|
local _n _m _prefix _x
|
|
# Build the map of "mac name [mac name] [...]"
|
|
en_prep
|
|
|
|
# Don't report any other errors if we haven't been asked to do anything.
|
|
if [ ${#en_orig} -eq 0 ]; then
|
|
warn "Unable to locate any of the specified ethname_\*_mac addresses."
|
|
exit 0
|
|
fi
|
|
|
|
# Rename interfaces; first into en_tmp_$_n with _n = 0, 1, ... to avoid any
|
|
# possible collision with the desired names. (ex. ue0 -> ue1; ue1 -> ue0
|
|
# renaming.)
|
|
_prefix=en_$$_
|
|
_n=0
|
|
for _x in ${en_orig}; do
|
|
if fix_name ${_x} ${_prefix}${_n}; then
|
|
_n=$((_n+1))
|
|
elif [ $? -eq 1 ]; then
|
|
info "Skipping rename of [${_x}]: ${en_str}"
|
|
else
|
|
warn "Error during rename of [${_x}]: ${en_str}"
|
|
fi
|
|
done
|
|
|
|
# Loop back over renamed devices and lookup their desired names.
|
|
_m=0
|
|
while [ ${_m} -lt ${_n} ]; do
|
|
fix_name ${_prefix}${_m} || \
|
|
warn "Error during renaming process. Stranded [${_prefix}${_m}]."
|
|
_m=$((_m+1))
|
|
done
|
|
}
|
|
|
|
run_rc_command "$1"
|
|
|
|
# vim: et:ts=4:sw=4
|