exit sai do processo atual do shell¹.
Em $(resolve_ip) , resolve_ip está sendo executado em um processo de subshell.
Você pode fazer:
my_ip=$(resolve_ip) || exit
master_ip=$(resolve_ip "$hostname") || exit
if [ "$my_ip" = "$master_ip" ]; ...
Para o shell principal sair (com o mesmo código de saída que o subshell) quando a subshell sair com um status de saída diferente de zero.
Além disso, como resolve_ip é executado em um ambiente subshell, as variáveis $ip e $host não sobreviverão depois que a subshell retornar.
Observe também que o (...) in (>&2 echo "Error: $1") também inicia uma subshell. Não é realmente necessário aqui, a menos que você queira cobrir o caso em que stderr é um pipe quebrado e gravar a mensagem de erro causaria uma entrega SIGPIPE para o processo principal do shell, pois echo está embutido.
Aqui, em vez de retornar a saída ao stdout, você poderia retorná-lo armazenando-o em variáveis fornecidas pelo usuário:
resolve_ip (){ # args: ip_var [host]
if [ "$#" -eq 1 ]; then
host=localhost
eval "$1="'$(dig +short myip.opendns.com @resolver1.opendns.com)'
else
host=$2
eval "$1="'$(dig +short "$2")'
fi
if eval '[ -z "${'"$1"'}" ]'; then
error "Could not resolve $host"
fi
}
# ...
resolve_ip my_ip
resolve_ip master_ip "$hostname"
if [ "$my_ip" = "$master_ip" ]; ...
¹ Estritamente ambientes subshell não precisam ser implementados com processos filho, e alguns shells como ksh93 não são como uma otimização, mas ainda assim exit só sai da subshell, não o principal Concha. ksh93 no entanto tem uma ${ ...; } forma ou substituição de comando que não envolve um ambiente subshell, então exit em que sairia do shell principal.