Vendor the command set the menu and shell depend on
This commit is contained in:
Executable
+319
@@ -0,0 +1,319 @@
|
||||
#!/bin/bash
|
||||
|
||||
# blob:summary=Show or configure the system DNS provider
|
||||
# blob:args=[Cloudflare|Google|DHCP|Custom]
|
||||
# blob:examples=blob dns | blob dns Cloudflare | blob dns Custom
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Whenever this runs as root — invoked directly through the passwordless
|
||||
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
|
||||
# where a bare helper resolves, and a dev link (etc/sudoers.d/blob-dev-path)
|
||||
# prepends a user-writable checkout bin/ to it. Every helper this script calls
|
||||
# by bare name (dirname, install, tee, rm, nmcli, systemctl, awk) is a system
|
||||
# tool, never an blob-* command, so pin PATH to trusted system directories
|
||||
# and keep root from resolving one out of that checkout. The unprivileged
|
||||
# wrapper phase keeps the caller's PATH so it can still find sudo/pkexec.
|
||||
if (( EUID == 0 )); then
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||
fi
|
||||
|
||||
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-blob-network-dns.conf
|
||||
|
||||
provider_from_arg() {
|
||||
case "${1:-}" in
|
||||
Cloudflare | cloudflare)
|
||||
echo "Cloudflare"
|
||||
;;
|
||||
Google | google)
|
||||
echo "Google"
|
||||
;;
|
||||
DHCP | dhcp)
|
||||
echo "DHCP"
|
||||
;;
|
||||
Custom | custom)
|
||||
echo "Custom"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The path etc/sudoers.d/blob-network-dns names. The privileged half always runs from
|
||||
# there rather than from whichever copy was invoked, so the rule matches even
|
||||
# where $BLOB_PATH points at a checkout.
|
||||
PACKAGED_PATH=/usr/bin/blob-network-dns
|
||||
|
||||
# True when sudo would run this exact command without stopping for a password.
|
||||
# `sudo -l` on its own reports whether a command is permitted, which the blanket
|
||||
# %wheel rule answers yes to for everything; the long listing prints the matched
|
||||
# entry's tags, so !authenticate is the grant in etc/sudoers.d/blob-network-dns and
|
||||
# nothing else. Listing runs nothing and, under -n, prompts for nothing, so a
|
||||
# machine whose blob-settings predates that file falls through to polkit
|
||||
# instead of dying on a password prompt it has no terminal to show.
|
||||
sudo_grants_passwordless() {
|
||||
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
|
||||
}
|
||||
|
||||
require_root() {
|
||||
if (( EUID == 0 )); then
|
||||
return
|
||||
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
|
||||
# A terminal can carry sudo's own password prompt. Without one, sudo is
|
||||
# right only where the grant reaches; polkit can at least put a prompt on
|
||||
# screen, and offer to authenticate as someone else.
|
||||
exec sudo "$PACKAGED_PATH" "$@"
|
||||
else
|
||||
exec pkexec "$PACKAGED_PATH" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
networkmanager_global_dns() {
|
||||
[[ -f $NM_DNS_CONF ]] || return 0
|
||||
|
||||
awk -F= '
|
||||
/^[[:space:]]*#/ { next }
|
||||
/^[[:space:]]*\[global-dns-domain-\*\][[:space:]]*$/ { in_default = 1; next }
|
||||
/^[[:space:]]*\[/ { in_default = 0 }
|
||||
in_default && /^[[:space:]]*servers[[:space:]]*=/ {
|
||||
value = $0
|
||||
sub(/^[^=]*=/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
' "$NM_DNS_CONF"
|
||||
}
|
||||
|
||||
resolved_dns() {
|
||||
awk -F= '
|
||||
/^[[:space:]]*#/ { next }
|
||||
/^[[:space:]]*DNS[[:space:]]*=/ {
|
||||
value=$0
|
||||
sub(/^[^=]*=/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
' /etc/systemd/resolved.conf 2>/dev/null || true
|
||||
}
|
||||
|
||||
current_dns_provider() {
|
||||
local dns=""
|
||||
local compact=""
|
||||
|
||||
dns=$(networkmanager_global_dns)
|
||||
if [[ -z $(printf '%s' "$dns" | tr -d '[:space:],') ]]; then
|
||||
dns=$(resolved_dns)
|
||||
fi
|
||||
|
||||
compact=$(printf '%s' "$dns" | tr -d '[:space:],')
|
||||
|
||||
if [[ -z $compact ]]; then
|
||||
echo "DHCP"
|
||||
elif [[ $dns == *"cloudflare-dns.com"* || $dns == *"1.1.1.1"* || $dns == *"2606:4700:4700::1111"* ]]; then
|
||||
echo "Cloudflare"
|
||||
elif [[ $dns == *"dns.google"* || $dns == *"8.8.8.8"* || $dns == *"2001:4860:4860::8888"* ]]; then
|
||||
echo "Google"
|
||||
else
|
||||
echo "Custom"
|
||||
fi
|
||||
}
|
||||
|
||||
normalize_servers() {
|
||||
printf '%s\n' "$*" | tr ',\t\n' ' ' | xargs | tr ' ' ','
|
||||
}
|
||||
|
||||
split_dns_servers() {
|
||||
local servers="$1"
|
||||
local server clean
|
||||
ipv4_dns=""
|
||||
ipv6_dns=""
|
||||
|
||||
for server in ${servers//,/ }; do
|
||||
clean=${server#dns+tls://}
|
||||
clean=${clean#dns+udp://}
|
||||
clean=${clean%%#*}
|
||||
clean=${clean#[}
|
||||
clean=${clean%]}
|
||||
|
||||
[[ -n $clean ]] || continue
|
||||
if [[ $clean == *:* ]]; then
|
||||
ipv6_dns+="${ipv6_dns:+ }$clean"
|
||||
else
|
||||
ipv4_dns+="${ipv4_dns:+ }$clean"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
write_networkmanager_dns() {
|
||||
local servers="$1"
|
||||
|
||||
install -d -m 0755 "$(dirname "$NM_DNS_CONF")"
|
||||
# blob:heredoc-expands paths=none -- $servers is a normalized, single-line
|
||||
# DNS server list written as data, not a path or command; nothing user-writable
|
||||
# is resolved or executed from the root-owned drop-in.
|
||||
cat >"$NM_DNS_CONF" <<EOF
|
||||
# Managed by blob-network-dns. Remove this file or run blob dns DHCP to use DHCP DNS again.
|
||||
[global-dns]
|
||||
|
||||
[global-dns-domain-*]
|
||||
servers=$servers
|
||||
EOF
|
||||
}
|
||||
|
||||
clear_networkmanager_dns() {
|
||||
rm -f "$NM_DNS_CONF"
|
||||
}
|
||||
|
||||
networkmanager_dns_connection() {
|
||||
case "$1" in
|
||||
802-11-wireless|802-3-ethernet) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_connection_dns() {
|
||||
local uuid type
|
||||
local ipv4_dns="${1:-}"
|
||||
local ipv6_dns="${2:-}"
|
||||
|
||||
while IFS=: read -r uuid type; do
|
||||
[[ -n $uuid ]] || continue
|
||||
networkmanager_dns_connection "$type" || continue
|
||||
|
||||
nmcli connection modify "$uuid" \
|
||||
ipv4.ignore-auto-dns yes \
|
||||
ipv4.dns "$ipv4_dns" \
|
||||
ipv6.ignore-auto-dns yes \
|
||||
ipv6.dns "$ipv6_dns" \
|
||||
>/dev/null
|
||||
done < <(nmcli -t -f UUID,TYPE connection show)
|
||||
}
|
||||
|
||||
clear_connection_dns() {
|
||||
local uuid type
|
||||
|
||||
while IFS=: read -r uuid type; do
|
||||
[[ -n $uuid ]] || continue
|
||||
networkmanager_dns_connection "$type" || continue
|
||||
|
||||
nmcli connection modify "$uuid" \
|
||||
ipv4.ignore-auto-dns no \
|
||||
ipv4.dns "" \
|
||||
ipv6.ignore-auto-dns no \
|
||||
ipv6.dns "" \
|
||||
>/dev/null
|
||||
done < <(nmcli -t -f UUID,TYPE connection show)
|
||||
}
|
||||
|
||||
reapply_active_dns_connections() {
|
||||
local device type state
|
||||
|
||||
while IFS=: read -r device type state; do
|
||||
[[ -n $device && $state == connected ]] || continue
|
||||
case "$type" in
|
||||
wifi|ethernet)
|
||||
nmcli device reapply "$device" >/dev/null 2>&1 || true
|
||||
;;
|
||||
esac
|
||||
done < <(nmcli -t -f DEVICE,TYPE,STATE device status)
|
||||
}
|
||||
|
||||
reload_dns_stack() {
|
||||
if systemctl is-active --quiet NetworkManager.service 2>/dev/null; then
|
||||
# Load the updated NetworkManager config first, then reapply the active
|
||||
# profiles. A single conf,dns-full reload here pushes the old active DNS
|
||||
# settings, making the shell toggle appear one selection behind.
|
||||
nmcli general reload conf >/dev/null 2>&1 || systemctl reload NetworkManager.service 2>/dev/null || true
|
||||
reapply_active_dns_connections
|
||||
fi
|
||||
|
||||
systemctl reload systemd-resolved.service 2>/dev/null || systemctl restart systemd-resolved.service
|
||||
|
||||
if systemctl is-active --quiet NetworkManager.service 2>/dev/null; then
|
||||
# A resolved reload/restart can leave per-link DNS stale or empty; ask
|
||||
# NetworkManager to publish DNS after resolved has reread its config.
|
||||
nmcli general reload dns-full >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: blob-network-dns [Cloudflare|Google|DHCP|Custom]" >&2
|
||||
}
|
||||
|
||||
if (( $# == 0 )); then
|
||||
current_dns_provider
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (( $# > 1 )); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! provider=$(provider_from_arg "$1"); then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_root "$provider"
|
||||
|
||||
case "$provider" in
|
||||
Cloudflare)
|
||||
write_networkmanager_dns "1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001"
|
||||
set_connection_dns "1.1.1.1 1.0.0.1" "2606:4700:4700::1111 2606:4700:4700::1001"
|
||||
tee /etc/systemd/resolved.conf >/dev/null <<'EOF'
|
||||
[Resolve]
|
||||
DNS=1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com
|
||||
FallbackDNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net
|
||||
DNSOverTLS=opportunistic
|
||||
EOF
|
||||
;;
|
||||
|
||||
Google)
|
||||
write_networkmanager_dns "8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844"
|
||||
set_connection_dns "8.8.8.8 8.8.4.4" "2001:4860:4860::8888 2001:4860:4860::8844"
|
||||
tee /etc/systemd/resolved.conf >/dev/null <<'EOF'
|
||||
[Resolve]
|
||||
DNS=8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google
|
||||
FallbackDNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net
|
||||
DNSOverTLS=opportunistic
|
||||
EOF
|
||||
;;
|
||||
|
||||
DHCP)
|
||||
clear_networkmanager_dns
|
||||
clear_connection_dns
|
||||
tee /etc/systemd/resolved.conf >/dev/null <<'EOF'
|
||||
[Resolve]
|
||||
DNSOverTLS=no
|
||||
EOF
|
||||
;;
|
||||
|
||||
Custom)
|
||||
echo "Enter your DNS servers (space-separated, e.g. '192.168.1.1 1.1.1.1'):"
|
||||
if ! read -r dns_servers; then
|
||||
dns_servers=""
|
||||
fi
|
||||
|
||||
dns_servers=$(normalize_servers "$dns_servers")
|
||||
if [[ -z $dns_servers ]]; then
|
||||
echo "Error: No DNS servers provided." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
split_dns_servers "$dns_servers"
|
||||
write_networkmanager_dns "$dns_servers"
|
||||
set_connection_dns "$ipv4_dns" "$ipv6_dns"
|
||||
# blob:heredoc-expands paths=none -- $dns_servers is a normalized,
|
||||
# single-line DNS server list; the //,/ turns its comma separators into the
|
||||
# spaces resolved.conf wants. It is written as data, not a path or command.
|
||||
tee /etc/systemd/resolved.conf >/dev/null <<EOF
|
||||
[Resolve]
|
||||
DNS=${dns_servers//,/ }
|
||||
FallbackDNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
|
||||
reload_dns_stack
|
||||
Reference in New Issue
Block a user