Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Manually change /etc/resolv.conf but protect it from meddling by NetworkManager et al
#!/bin/bash
# Update /etc/resolv.conf but keep it unwritable, even by root-owned processes.
# Useful to stop WiFi and/or DHCP from messing with it, and quite essential when
# there are multiple managers for different interface types.
#
# Because symlinks don't work, instead we bind-mount an appropriate file onto
# /etc/resolv.conf, chosen from those in ~/.resolvconf.d/.
#
# Since BIND is running locally, almost all the time that will work, so it's the
# default if nothing else is loaded.
erc=/etc/resolv.conf
xdir=${HOME:-~martin.k}/.resolvconf.d/
(( EUID == 0 )) || exec sudo $0 "$@"
false=0 true=1
dhcp=true
role=
verbose=true _v=
dry_run=false _n=
debug=false _x=
die() {
RESULT=$? SUCCESS=0 FAILURE=1 OK=0 USAGE=64 DATAERR=65 NOINPUT=66 NOUSER=67
NOHOST=68 UNAVAILABLE=69 SOFTWARE=70 OSERR=71 OSFILE=72 CANTCREAT=73
IOERR=74 TEMPFAIL=75 PROTOCOL=76 NOPERM=77 CONFIG=78
((e=${1#EX*_})) ; shift
echo >&2 "$*"
exit $e
}
[[ "$*" = "-h" || "$*" = "--h"* && "--help" = "$*"* ]] && { cat <<EndOfHelp ; exit 0 ; }
$0 --help
$0 [-q | -v[v...]] [-d DIR] [-r ROLE]
Roles are: $( cd $xdir ; echo * )
EndOfHelp
true=1 false=0
while (($#)) ; do
case $1 in
(--) shift ; break ;;
(-) role=localhost ;;
(-q) verbose=false _v= ;;
(-v) verbose=true _v=${_v:--}v ;;
(-D|--dir) xdir=$2 ; shift ;;
(-d|--dhcp) dhcp=true ;;
(-n|--dry*run) dry_run=true _n=-n ;;
(-nd|--no-dhcp|--nodhcp) dhcp=false ;;
(-r|--role) role=$2 ; shift ;;
(-x|--debug) debug=true _x=-x ;;
(-[Dr]?*) set -- "${1:0:2}" "${1:2}" "${@:2}" ; continue ;;
(-[^-]?*) set -- "${1:0:2}" "-${1:2}" "${@:2}" ; continue ;;
(--*=*) set -- "${1%%=*}" "${1#*=}" "${@:2}" ; continue ;;
(-*) die EX_USAGE "Invalid option '$1'; try '${0##*/} --help'" ;;
(*) break ;;
esac
shift
done
: ${role:=$1}
:|sort_array() {
local ___cmp=$1 ___v=$2 ___n ___i ___j a b
eval "((___n=\${!$___v[@]}))"
local ___vi="$___v[___i]"
local ___vj="$___v[___j]"
# stretch the size for sparse arrays
for (( ___i=0 ; ___i<___n ; ++___i )) do
[[ -n "${!___vi+X}" ]] || ((++___n))
done
# plain bubble-sort
for (( ___i=0 ; ___i<___n ; ++___i )) do
[[ -n "${!___vi+X}" ]] || continue
for (( ___j=___i+1 ; ___j<___n ; ++___j )) do
[[ -n "${!___vj+X}" ]] || continue
set -- "${!___vi}" "${!___vj}"
eval "$___cmp" || {
printf -v "$___vi" -- %s "$2"
printf -v "$___vj" -- %s "$1"
}
done
done
}
:!sort_array() {
local ___cmp=$1 ___vptr=$2 ___changed=0
local ___vref="$___vptr[@]"
local -a ___array=("${!___vref}")
local ___n ___i ___j a b
for ((___n=${#___array[@]},___i=0;___i<___n;___i++)) do
for ((___j=___i+1;___j<___n;___j++)) do
set -- "${___array[___i]}" "${___array[___j]}"
eval "$___cmp" ||
___array[___i]="$b" \
___array[___j]="$a" \
___changed=1
done
done
((___changed)) && eval "$___vptr=(\"\${___array[@]}\")"
}
___eswap() {
# NB relies on dynamic scoping for ___array
local x="${___array[$1]}"
___array[$1]="${___array[$2]}"
___array[$2]="$x"
}
sort_array() {
local ___cmp=$1
local -n ___array=$2
local ___n ___i ___j
for ((___n=${#___array[@]},___i=0;___i<___n;___i++)) do
for ((___j=___i+1;___j<___n;___j++)) do
set -- "${___array[___i]}" "${___array[___j]}"
eval "$___cmp" ||
___eswap $___i $___j
done
done
}
if $debug || :
then
no_debug() {
local _oldx=$-
set +x
"$@"
local r=$?
if [[ $_oldx = *x* ]] ; then set -x ; else set +x ; fi
return $r
}
with_debug() {
local _oldx=$-
set -x
"$@"
local r=$?
if [[ $_oldx = *x* ]] ; then set -x ; else set +x ; fi
return $r
}
else
no_debug() {
"$@"
}
with_debug() {
"$@"
}
fi
case $role in
none)
ns=()
;;
dhcp|'')
rolefiles=($( find /var/lib/dhcp /var/lib/NetworkManager \( -name \*.leases -o -name \*.lease \) -mmin -1440 -print ))
(( ${#rolefiles[@]} )) || die EX_TEMPFAIL "Can't find a recent DHCP lease (less than 2 hours old)"
no_debug sort_array '[[ $2 -nt $1 ]]' rolefiles
$debug && ls -ldU "${rolefiles[@]}"
ns=()
printf -v tn '%(%Y%m%d%H%M%S)T' -1
while read t lease
do
(( t>=tn )) &&
IFS=, read -r -a ns <<<"$lease"
done < <(
# extract all the known leases; fold up each one into a single line,
# then prefix that line with its expiry time as yyyymmddHHMMSS; then
# sort by those times newest-last, so that later ones will override
# older ones
sed -n 's#^ *##
/^#/d
/{/{
s#.*##
x
d
}
/}/!{
H
d
}
x
s#\n# #g
s#\(.*;\) expire[^;]*\([12][09][0-9][0-9]\)/\([01][0-9]\)/\([0-3][0-9]\) \([0-2][0-9]\):\([0-5][0-9]\):\([0-5][0-9]\)#\2\3\4\5\6\7 &#
s# .* option domain-name-servers *\([0-9.,]*\);.*# \1#p
' "${rolefiles[@]}" </dev/null |
sort -r
)
# # Sort rolefiles into time-order
# ns=($( sed -ne '
# /^}/bo
# s/^ *//
# /^option /!d
# s/^option *//
# /^domain-name-servers *[0-9.,]*;*$/!d
# /^domain-name[^ ]* *[0-9.,]*;*$/!d
# s/^[^ ]* *//
# s/;$//
# s/,/ /g
# H
# bl
# :o
# s/.*//
# x
# bl
# :l
# $p
# ' "${rolefiles[@]}" </dev/null ))
(( ${#ns[@]} )) || echo >&2 "No 'option domain-name*' in ${rolefiles[*]}"
;;
*) ns=( ${role//,/\ } ) ;;
esac
# (Sort and) prune duplicates
for i in ${!ns[@]}
do
[[ ${ns[i]-__UnSeT__uNsEt__} = __UnSeT__uNsEt__ ]] ||
for j in ${!ns[@]}
do
(( i<j )) || continue
if [[ ${ns[i]} = ${ns[j]} ]]
then
unset 'ns[j]'
elif [[ ${ns[i]} > ${ns[j]} ]] && false
then
ti=${ns[i]} tj=${ns[j]}
ns[i]=$tj ns[j]=$ti
fi
done
done
any_match() {
local f="$1" g ; shift
for g do
[[ $f =~ $g ]] && return 0
done
return 1
}
all_match() {
local f="$1" g ; shift
for g do
[[ $f =~ $g ]] || return 1
done
return 0
}
if all_match 'localhost|0.0.0.0|127.0.0.1' "${ns[@]}"
then
# includes case where ns array is empty
frc=
if $dry_run
then
echo >&2 "Would use local resolver"
elif $verbose
then
echo >&2 "Using local resolver"
fi
else
nsx="${ns[*]}"
frc=$xdir${nsx//\ /+}
if [[ -e $frc ]]
then
if [[ ! -f $frc ]]
then die EX_NOINPUT "'$frc' is not a regular file"
elif ! grep -qs '^nameserver ' "$frc"
then die EX_PROTOCOL "'$frc' does not contain a 'nameserver' statement"
fi
if $dry_run
then
echo >&2 "Would use $frc"
elif $verbose
then
echo >&2 "Using '$frc'"
fi
else
for ip in ${ns[@]} ; do [[ -z ${ip//[.0-9]/} && $ip = *.*.*.* && $ip != *.*.*.*.* && .$ip != *..* ]] || die EX_PROTOCOL "Nameserver '$ip' isn't IPv4 addresses" ; done
if $dry_run
then
echo >&2 "Would create '$frc' with [${ns[*]}]"
else
$verbose && echo >&2 "Creating '$frc' with [${ns[*]}]"
printf 'nameserver %s\n' "${ns[@]}" >"$frc"
#for ip in ${ns[@]} ; do echo "nameserver $ip" ; done >"$frc"
fi
fi
if $dry_run
then
echo >&2 "Would make '$frc' unwritable"
# if [[ -h $frc ]]
# then hrc=$( readlink -e "$frc" ) || die EX_OSERR "Can't canonicalize '$frc'"
# [[ $frc != $hrc ]] && echo >&2 "Have canonicalized '$frc' to '$hrc'"
# else hrc=$frc
# fi
# echo >&2 "Would enforce immutability on '$hrc'"
else
[[ ! -w $frc ]] || {
$verbose && echo >&2 "Making '$frc' unwritable"
chmod -c a-xw "$frc" || die EX_OSERR "Can't fix permissions on '$frc'"
}
# if [[ -h $frc ]]
# then hrc=$( readlink -e "$frc" ) || die EX_OSERR "Can't canonicalize $frc"
# $verbose && [[ $frc != $hrc ]] && echo >&2 "Have canonicalized '$frc' to '$hrc'"
# else hrc=$frc
# fi
# $verbose && echo >&2 "Enforcing immutability on '$hrc'"
# chattr +i "$hrc" || die EX_OSERR "Can't enforce immutability on '$hrc'"
fi
fi
if $dry_run
then
! grep -qs "$erc" /proc/mounts || echo >&2 "Would unmount current '$erc'"
[[ -z $frc ]] || echo >&2 "Would mount '$frc' on '$erc'"
else
! grep -qs "$erc" /proc/mounts || { echo >&2 "Unmounting current '$erc'"
umount "$erc" ||
die EX_OSERR "Can't unmount old '$erc'" ; }
# Note: bind-mounting with *different* options requires *two* mount
# commands; see the man page excerpt at the foot of this script.
[[ -z $frc ]] || { echo >&2 "Mounting '$frc' on '$erc'"
mount --bind "$frc" "$erc" ||
die EX_OSERR "Can't bind-mount '$frc' as new '$erc'"
mount -o remount,bind,ro "$frc" "$erc" ||
die EX_OSERR "Can't remount '$erc' with ro"
}
fi
((verbose)) && grep '^nameserver ' "$erc"
: <<EOF
[From «man mount» apropos «--bind»]
Note that the filesystem mount options will remain the same as those on
the original mount point, and cannot be changed by passing the -o option
along with --bind/--rbind. The mount options can be changed by a separate
remount command, for example:
mount --bind olddir newdir
mount -o remount,ro newdir
Note that behavior of the remount operation depends on the /etc/mtab file.
The first command stores the 'bind' flag to the /etc/mtab file and the
second command reads the flag from the file. If you have a system
without the /etc/mtab file or if you explicitly define source and target
for the remount command (then mount(8) does not read /etc/mtab), then you
have to use bind flag (or option) for the remount command too. For
example:
mount --bind olddir newdir
mount -o remount,ro,bind olddir newdir
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment