Node 2 Network Speeds Throttled and Reset on Reboot
What happened
In my home lab my node 2's integrated motherboard has a 1gb Ethernet port, but since I need faster speeds between nodes and my home network I added a USB 2.0 adapter because my PCI slots were already full. The issue is that every time I reboot my node, my USB 3.0 adapter defaults to 1gb speeds and I have to run a script to force it to 2.5gb optimal speeds.
Root cause
Hardware Reboot or Unexpected Shutdowns
The fix
Create a script that runs on boot to force my USB adapter to 2.5gb speeds if it's detected.
I already implemented this fix in my network speeds incident report, but that was a temporary fix and I didn't release it; it reset the speeds back to default on reboots, and since I was having overheating issues on node 2, I noticed the change after those issues.
Unraid has a feature where I can set up bash scripts to run on a schedule. I plan to implement a bash script that checks which eth port my USB device is on, determines the device's max capable speeds, and sets it at the start of the array (on boot).

Since I'm very novice at creating bash scripts, I used an LLM to help create a script that parses all from eth0 to eth4, then looks at their max rated speeds and sets them accordingly. It also has side features to force the Ethernet ports to run at the fastest speeds and a safety guard to revert to the original settings if the same client doesn't have a connection within 25 seconds.
#!/bin/bash
MAX_IF=4
APPLY=1
ASSUME_YES=1
FORCE=1
DETACH=1
LINK_TIMEOUT=25
LOGFILE=/var/log/nic-speed.log
# ------------------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
"") ;; # User Scripts passes an empty arg; ignore it
--apply) APPLY=1 ;;
-y|--yes) ASSUME_YES=1 ;;
--force) FORCE=1 ;;
--detach) DETACH=1 ;;
--max) MAX_IF="$2"; shift ;;
--timeout) LINK_TIMEOUT="$2"; shift ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
*) echo "unknown option: $1" >&2; exit 1 ;;
esac
shift
done
[ -t 0 ] || ASSUME_YES=1
ETHTOOL=$(command -v ethtool || echo /usr/sbin/ethtool)
[ -x "$ETHTOOL" ] || { echo "ethtool not found" >&2; exit 1; }
# ------------------------------------------------------------------ log ----
log() {
local msg="$*"
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$msg"
command -v logger >/dev/null 2>&1 && logger -t nic-speed "$msg"
}
echo "=============================================="
echo " Started Network Max Speed Set"
echo " $(date '+%Y-%m-%d %H:%M:%S') host: $(hostname)"
echo " mode: $( [ "$APPLY" -eq 1 ] && echo APPLY || echo REPORT-ONLY ) force: $FORCE scan: eth0-eth$MAX_IF"
echo "=============================================="
echo
# --------------------------------------------------------------- detach ----
# Re-exec detached from the controlling terminal / User Scripts window, with
# all output appended to LOGFILE. Losing the link then cannot abort the run.
if [ "$DETACH" -eq 1 ] && [ -z "$NICSPEED_CHILD" ]; then
args=()
[ "$APPLY" -eq 1 ] && args+=(--apply)
[ "$FORCE" -eq 1 ] && args+=(--force)
args+=(-y --max "$MAX_IF" --timeout "$LINK_TIMEOUT")
echo "Detaching. Output goes to $LOGFILE"
echo "Follow it with: tail -f $LOGFILE"
NICSPEED_CHILD=1 setsid nohup "$0" "${args[@]}" >> "$LOGFILE" 2>&1 < /dev/null &
exit 0
fi
# ---------------------------------------------------------------- parsing ---
list_modes() {
printf '%s\n' "$1" | awk -v want="$2:" '
index($0, want) { insec = 1 }
insec {
if (!index($0, want) && index($0, ":")) { insec = 0; next }
n = split($0, tok, /[ \t]+/)
for (i = 1; i <= n; i++) if (tok[i] ~ /^[0-9]+base/) print tok[i]
}'
}
max_of() { awk '{ s = $0; sub(/base.*/, "", s); if (s + 0 > m) m = s + 0 } END { print m + 0 }'; }
fmt() {
awk -v m="$1" 'BEGIN {
if (m + 0 <= 0) { print "-"; exit }
if (m + 0 < 1000) { printf "%dM\n", m; exit }
g = m / 1000
if (g == int(g)) printf "%dG\n", g; else printf "%.1fG\n", g
}'
}
# Bit positions from the kernel ETHTOOL_LINK_MODE_* enum.
mode_bit() {
case "$1" in
10baseT/Half) echo 0 ;; 10baseT/Full) echo 1 ;;
100baseT/Half) echo 2 ;; 100baseT/Full) echo 3 ;;
1000baseT/Half) echo 4 ;; 1000baseT/Full) echo 5 ;;
10000baseT/Full) echo 12 ;; 2500baseX/Full) echo 15 ;;
1000baseKX/Full) echo 17 ;; 10000baseKX4/Full) echo 18 ;;
10000baseKR/Full) echo 19 ;; 25000baseCR/Full) echo 31 ;;
25000baseKR/Full) echo 32 ;; 25000baseSR/Full) echo 33 ;;
1000baseX/Full) echo 41 ;; 10000baseCR/Full) echo 42 ;;
10000baseSR/Full) echo 43 ;; 10000baseLR/Full) echo 44 ;;
10000baseLRM/Full) echo 45 ;; 10000baseER/Full) echo 46 ;;
2500baseT/Full) echo 47 ;; 5000baseT/Full) echo 48 ;;
*) echo -1 ;;
esac
}
mask_from() { # $1 = ethtool output, $2 = label -> hex mask on stdout
local mask=0 m b
while read -r m; do
[ -z "$m" ] && continue
b=$(mode_bit "$m")
[ "$b" -ge 0 ] && [ "$b" -lt 63 ] && mask=$(( mask | (1 << b) ))
done < <(list_modes "$1" "$2")
printf '0x%x' "$mask"
}
# ------------------------------------------------------------ environment ---
SESSION_IF=""
if [ -n "$SSH_CONNECTION" ]; then
peer=$(echo "$SSH_CONNECTION" | awk '{print $1}')
SESSION_IF=$(ip route get "$peer" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')
fi
DEFAULT_IF=$(ip route show default 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')
resolve_master() {
local d="$1" m
while [ -L "/sys/class/net/$d/master" ]; do
m=$(basename "$(readlink -f "/sys/class/net/$d/master")")
[ "$m" = "$d" ] && break
d="$m"
done
echo "$d"
}
is_critical() {
local up; up=$(resolve_master "$1")
if [ -n "$SESSION_IF" ] && { [ "$1" = "$SESSION_IF" ] || [ "$up" = "$SESSION_IF" ]; }; then return 0; fi
if [ -n "$DEFAULT_IF" ] && { [ "$1" = "$DEFAULT_IF" ] || [ "$up" = "$DEFAULT_IF" ]; }; then return 0; fi
return 1
}
wait_link() { # $1 = dev, $2 = seconds -> echoes negotiated Mb/s, "" if down
local dev="$1" secs="$2" i st spd
for (( i = 0; i < secs; i++ )); do
sleep 1
st=$("$ETHTOOL" "$dev" 2>/dev/null)
if printf '%s\n' "$st" | grep -q "Link detected: yes"; then
spd=$(printf '%s\n' "$st" | awk -F': ' '/[[:space:]]Speed:/ {print $2; exit}')
spd=${spd%Mb/s}
case "$spd" in ''|*[!0-9]*) ;; *) echo "$spd"; return 0 ;; esac
fi
done
echo ""
return 1
}
# ---------------------------------------------------------------- report ----
TODO_IF=(); TODO_MASK=(); TODO_OLD=(); TODO_FROM=(); TODO_TO=(); TODO_CRIT=(); TODO_ADD=()
CHANGED=(); UNCHANGED=(); FAILED=()
printf "%-7s %-9s %-6s %-8s %-10s %-9s %-8s %s\n" \
IFACE DRIVER LINK CURRENT ADVERTISED SUPPORTED PARTNER NOTE
printf -- "--------------------------------------------------------------------------------\n"
best=0
for n in $(seq 0 "$MAX_IF"); do
dev="eth$n"
[ -e "/sys/class/net/$dev" ] || continue
out=$("$ETHTOOL" "$dev" 2>/dev/null) || continue
sup=$(list_modes "$out" "Supported link modes" | max_of)
adv=$(list_modes "$out" "Advertised link modes" | max_of)
par=$(list_modes "$out" "Link partner advertised link modes" | max_of)
link=$(printf '%s\n' "$out" | awk -F': ' '/Link detected:/ {print $2; exit}')
autoneg=$(printf '%s\n' "$out" | awk -F': ' '/Supports auto-negotiation:/ {print $2; exit}')
cur=$(printf '%s\n' "$out" | awk -F': ' '/[[:space:]]Speed:/ {print $2; exit}'); cur=${cur%Mb/s}
case "$cur" in ''|*[!0-9]*) cur_h="-" ;; *) cur_h=$(fmt "$cur") ;; esac
drv="?"
[ -L "/sys/class/net/$dev/device/driver" ] && \
drv=$(basename "$(readlink -f "/sys/class/net/$dev/device/driver")")
note=""
if [ "$sup" -gt "$adv" ]; then
if [ "$autoneg" != "Yes" ]; then
note="capped, but no autoneg"
else
newmask=$(mask_from "$out" "Supported link modes")
oldmask=$(mask_from "$out" "Advertised link modes")
crit=0; is_critical "$dev" && crit=1
if [ "$newmask" = "0x0" ]; then
note="capped, no mappable modes"
elif [ "$crit" -eq 1 ] && [ "$FORCE" -eq 0 ]; then
note="capped -> SKIPPED (carries session; use --force --detach)"
else
# Exactly which link modes this run will add.
addmodes=$(comm -13 \
<(list_modes "$out" "Advertised link modes" | sort -u) \
<(list_modes "$out" "Supported link modes" | sort -u) \
| tr '\n' ' ')
addmodes=${addmodes% }
note="capped -> will raise to $(fmt "$sup")"
[ "$crit" -eq 1 ] && note="$note (SESSION NIC, watchdogged)"
TODO_IF+=("$dev"); TODO_MASK+=("$newmask")
TODO_OLD+=("$oldmask"); TODO_FROM+=("$adv")
TODO_TO+=("$sup"); TODO_CRIT+=("$crit")
TODO_ADD+=("${addmodes:-none}")
fi
if [ "$par" -gt 0 ] && [ "$par" -lt "$sup" ]; then
note="$note [partner tops out at $(fmt "$par")]"
fi
fi
fi
printf "%-7s %-9s %-6s %-8s %-10s %-9s %-8s %s\n" \
"$dev" "$drv" "${link:-?}" "$cur_h" "$(fmt "$adv")" "$(fmt "$sup")" "$(fmt "$par")" "$note"
[ "$sup" -gt "$best" ] && best=$sup
done
echo
echo "Fastest NIC in this box: $(fmt "$best")"
# ----------------------------------------------------------------- apply ----
if [ "$APPLY" -eq 0 ]; then
if [ "${#TODO_IF[@]}" -gt 0 ]; then
echo
echo "Report-only mode - NO CHANGES APPLIED. Would change:"
for i in "${!TODO_IF[@]}"; do
echo " ${TODO_IF[$i]}: ceiling $(fmt "${TODO_FROM[$i]}") -> $(fmt "${TODO_TO[$i]}"), adding ${TODO_ADD[$i]}"
echo " ethtool -s ${TODO_IF[$i]} autoneg on advertise ${TODO_MASK[$i]}"
done
fi
exit 0
fi
if [ "${#TODO_IF[@]}" -eq 0 ]; then
echo; echo "Nothing to raise."; exit 0
fi
echo
echo "About to re-advertise (each link bounces for a few seconds):"
for i in "${!TODO_IF[@]}"; do
printf " %s: ceiling %s -> %s%s\n" \
"${TODO_IF[$i]}" "$(fmt "${TODO_FROM[$i]}")" "$(fmt "${TODO_TO[$i]}")" \
"$([ "${TODO_CRIT[$i]}" -eq 1 ] && echo ' <-- carries this session')"
printf " adding link modes : %s\n" "${TODO_ADD[$i]}"
printf " advertise mask : %s (rollback to %s)\n" \
"${TODO_MASK[$i]}" "${TODO_OLD[$i]}"
done
if [ "$ASSUME_YES" -eq 0 ]; then
printf "Proceed? [y/N] "
read -r ans
case "$ans" in y|Y|yes|YES) ;; *) echo "Aborted."; exit 1 ;; esac
fi
for i in "${!TODO_IF[@]}"; do
dev="${TODO_IF[$i]}"; new="${TODO_MASK[$i]}"; old="${TODO_OLD[$i]}"
top="${TODO_TO[$i]}"; was="${TODO_FROM[$i]}"
log "$dev: CHANGE - adding link modes: ${TODO_ADD[$i]}"
log "$dev: CHANGE - advertise mask $old -> $new (ceiling $(fmt "$was") -> $(fmt "$top"))"
if ! "$ETHTOOL" -s "$dev" autoneg on advertise "$new" 2>&1; then
log "$dev: FAILED - driver rejected mask $new, NO CHANGE APPLIED (still $(fmt "$was"))"
FAILED+=("$dev: rejected mask, unchanged at $(fmt "$was")")
continue
fi
spd=$(wait_link "$dev" "$LINK_TIMEOUT")
if [ -n "$spd" ]; then
if [ "$spd" -gt "$was" ]; then
log "$dev: APPLIED - link speed $(fmt "$was") -> $(fmt "$spd") (hardware max $(fmt "$top"))"
CHANGED+=("$dev: $(fmt "$was") -> $(fmt "$spd") [added ${TODO_ADD[$i]}]")
else
log "$dev: APPLIED mask, but link renegotiated at $(fmt "$spd") - speed unchanged from $(fmt "$was")"
UNCHANGED+=("$dev: still $(fmt "$spd") despite advertising up to $(fmt "$top")")
fi
else
log "$dev: NO LINK after ${LINK_TIMEOUT}s - ROLLING BACK mask $new -> $old"
"$ETHTOOL" -s "$dev" autoneg on advertise "$old" 2>&1
spd=$(wait_link "$dev" "$LINK_TIMEOUT")
if [ -n "$spd" ]; then
log "$dev: ROLLED BACK - restored to $(fmt "$spd"), no net change"
FAILED+=("$dev: would not link at $(fmt "$top"), rolled back to $(fmt "$spd")")
else
log "$dev: STILL DOWN after rollback - needs manual attention: ethtool -s $dev autoneg on"
FAILED+=("$dev: DOWN after rollback - manual fix needed")
fi
fi
done
echo
echo "=============================================="
echo " Network Max Speed Set - summary"
echo "=============================================="
if [ "${#CHANGED[@]}" -gt 0 ]; then
echo " Changes applied:"
for e in "${CHANGED[@]}"; do echo " $e"; done
else
echo " Changes applied: none"
fi
[ "${#UNCHANGED[@]}" -gt 0 ] && { echo " No speed gain:"; for e in "${UNCHANGED[@]}"; do echo " $e"; done; }
[ "${#FAILED[@]}" -gt 0 ] && { echo " Problems:"; for e in "${FAILED[@]}"; do echo " $e"; done; }
echo
echo "Unraid runs from RAM; this does not survive a reboot."
echo "To persist, add the ethtool lines above to /boot/config/go."
exit 0After running this script standalone it was a success
Before

Run networkSpeedInit.sh

After

Results
Success. Now all I need to do is set the script to run every time the array starts and test it on a reboot.
With Unraid this is easy: I go to User Scripts and set it to run at the start of the array

I would run it only at the first start, but it doesn't harm anything if it runs anytime I start my array since all it does is set the device to its max rated speeds.
Overall this was a success, and I gained experience with startup scripts and bash scripting.
// related project
Home Lab β// referenced in
β© Linux Container failing after Network Drive SMB unmount