Created
July 2, 2026 18:20
-
-
Save VSharapov/58d370b37e0491c66b1f8ae6a27ec825 to your computer and use it in GitHub Desktop.
Give same-subnet secondary NICs a distinct DHCP hostname so the bare name tracks the primary
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -Eeuo pipefail | |
| primary-line() { ip -o route show default | awk '{m=0;for(i=1;i<=NF;i++)if($i=="metric")m=$(i+1);print m"\t"$0}' | sort -n | head -1 | cut -f2-; } | |
| primary-iface() { primary-line | grep -oP 'dev \K\S+'; } | |
| default-gateway() { primary-line | grep -oP 'via \K\S+'; } | |
| iface-subnet() { ip -o -4 route show dev "$1" scope link proto kernel | awk '{print $1; exit}'; } | |
| primary-subnet() { iface-subnet "$(primary-iface)"; } | |
| secondary-ifaces() { | |
| local p net i | |
| p="$(primary-iface)"; net="$(iface-subnet "$p")" | |
| for i in $(ls /sys/class/net); do | |
| [ "$i" = "$p" ] && continue | |
| [ "$(iface-subnet "$i")" = "$net" ] && echo "$i" | |
| done | |
| } | |
| sys-hostname() { hostnamectl --static; } | |
| desired-hostname() { echo "${DESIRED_HOSTNAME:-$(sys-hostname)}-$1"; } | |
| conn-for-iface() { nmcli -t -g GENERAL.CONNECTION device show "$1"; } | |
| current-dhcp-hostname() { nmcli -g ipv4.dhcp-hostname connection show "$1"; } | |
| localdomain() { resolvectl domain "$1" 2>/dev/null | sed 's/.*): //' | awk '{print $1}'; } | |
| run() { | |
| if [ "${MODE:-preview}" = apply ]; then "$@"; return; fi | |
| local a q=''; for a in "$@"; do q+=" ${a@Q}"; done | |
| echo " DRY-RUN: would run:$q" | |
| } | |
| rehost-hint() { | |
| echo " make it live (safe even over SSH on $1): tmux new-session -d -s dhcp-rehost-$1 'sudo nmcli connection down \"$2\"; sleep 2; sudo nmcli connection up \"$2\"'" | |
| } | |
| dot-warn() { | |
| local base p ld | |
| base="${DESIRED_HOSTNAME:-$(sys-hostname)}" | |
| case "$base" in *.*) | |
| p="$(primary-iface)"; ld="$(localdomain "$p")" | |
| echo "WARNING: Your hostname is $base -- depending on the DHCP server / gateway on interface $p, this is likely to resolve as ${base%%.*}.${ld:-<localdomain>}" >&2 | |
| ;; esac | |
| } | |
| set-plan() { | |
| local secs i conn want have | |
| secs="$(secondary-ifaces)" | |
| [ -z "$secs" ] && { echo "no same-subnet secondary interfaces; nothing to do" >&2; [ "${MODE:-}" = apply ] && exit 3; exit 0; } | |
| dot-warn | |
| for i in $secs; do | |
| conn="$(conn-for-iface "$i")"; want="$(desired-hostname "$i")"; have="$(current-dhcp-hostname "$conn")" | |
| echo "[$i] conn \"$conn\" current=${have:-<unset>} desired=$want" | |
| [ "$have" = "$want" ] && { echo " already set; nothing to do"; continue; } | |
| run sudo nmcli connection modify "$conn" ipv4.dhcp-hostname "$want" | |
| rehost-hint "$i" "$conn" | |
| done | |
| } | |
| preview() { MODE=preview set-plan; } | |
| apply() { MODE=apply set-plan; } | |
| revert() { | |
| local secs i conn have noop=false | |
| secs="$(secondary-ifaces)" | |
| [ -z "$secs" ] && { echo "no same-subnet secondary interfaces; nothing to revert" >&2; exit 3; } | |
| for i in $secs; do | |
| conn="$(conn-for-iface "$i")"; have="$(current-dhcp-hostname "$conn")" | |
| echo "[$i] conn \"$conn\" current=${have:-<unset>}" | |
| [ -z "$have" ] && { echo " already default; nothing to do"; continue; } | |
| MODE=apply run sudo nmcli connection modify "$conn" ipv4.dhcp-hostname '' | |
| rehost-hint "$i" "$conn"; noop=true | |
| done | |
| $noop && { echo "all secondaries already default; nothing to do" >&2; exit 3 ; } || true | |
| } | |
| usage() { | |
| local me; me="$(basename "$0")" | |
| cat >&2 <<EOF | |
| $me - give same-subnet secondary NICs a distinct DHCP hostname so the bare name tracks the primary. | |
| subcommands: | |
| preview show what apply would change (DRY-RUN) | |
| apply set ipv4.dhcp-hostname on each secondary; prints how to make it live | |
| revert clear the override, back to default DHCP hostname | |
| usage [--verbose] this message${1:+ (with plumbing exposed for the curious)} | |
| EOF | |
| [ "${1:-}" = --verbose ] && cat >&2 <<EOF | |
| plumbing: | |
| primary-iface interface owning the lowest-metric default route | |
| default-gateway gateway IP of that route | |
| primary-subnet CIDR of the primary's IPv4 | |
| secondary-ifaces same-subnet interfaces (newline-delimited) | |
| desired-hostname <iface> \${DESIRED_HOSTNAME:-\$(hostnamectl --static)}-<iface> | |
| conn-for-iface <iface> NetworkManager connection on a device | |
| current-dhcp-hostname <conn> | |
| localdomain <iface> first search domain (resolvectl) | |
| environment: | |
| DESIRED_HOSTNAME override \$(hostnamectl --static) | |
| EOF | |
| } | |
| [ $# -eq 0 ] && { usage; exit 1; } | |
| "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment