#!/usr/bin/env bash # ============================================================================= # disable-ipv6.sh v2.0.0 # Completely disable IPv6 on Debian-family Linux with autodetection, # per-section fault isolation, idempotent writes, and post-change verification. # # Targets: Pop!_OS 22.04+, Zorin 17/18, Ubuntu 22.04+, Debian 12/13, # MX Linux, LMDE 6/7, Linux Mint 22+ # # Usage: # sudo bash disable-ipv6.sh apply + verify # sudo bash disable-ipv6.sh --verify verify only (use after reboot) # sudo bash disable-ipv6.sh --help # # Design notes: # * No `set -e` / `set -u` on purpose: one failing section must never abort # the rest. An ERR trap logs unexpected failures with line numbers instead. # * Every section runs through run_step(), which records PASS/FAIL and always # returns success so the script runs to completion. # * Writes are idempotent (temp file + cmp): unchanged files are left alone, # so re-runs are fast and don't spam backups or rebuild GRUB/initramfs. # * A reboot is required for the kernel cmdline change to take effect. # ============================================================================= if [ -z "${BASH_VERSION:-}" ]; then echo "ERROR: this script requires bash. Run: sudo bash $0" >&2 exit 1 fi set -o pipefail set -o errtrace umask 077 VERSION="2.0.0" PARAM="ipv6.disable=1" MODE="apply" BACKUP_ROOT="" IN_CONTAINER=0 GRUB_CHANGED=0 MODPROBE_CHANGED=0 NM_CHANGED=0 CRITICAL_FAIL=0 OPTIONAL_FAIL=0 STEPS_OK=0 STEPS_FAIL=0 STEPS_SKIP=0 declare -a REPORT_LINES=() declare -a SECTION_RESULTS=() SYSCTL_DROPIN="/etc/sysctl.d/zz-disable-ipv6.conf" NM_DROPIN="/etc/NetworkManager/conf.d/90-disable-ipv6.conf" NETPLAN_FILE="/etc/netplan/99-disable-ipv6.yaml" MODPROBE_FILE="/etc/modprobe.d/disable-ipv6.conf" # ---- colors ----------------------------------------------------------------- setup_colors() { if [[ -n "${NO_COLOR:-}" || ! -t 1 ]]; then C_RED=""; C_GRN=""; C_YEL=""; C_BLU=""; C_MAG=""; C_CYN="" C_BOLD=""; C_DIM=""; C_NC="" return fi C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m' C_BLU=$'\033[34m'; C_MAG=$'\033[35m'; C_CYN=$'\033[36m' C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'; C_NC=$'\033[0m' } log_info() { printf '%s[INFO]%s %s\n' "$C_CYN" "$C_NC" "$*"; } log_ok() { printf '%s[OK]%s %s\n' "$C_GRN" "$C_NC" "$*"; STEPS_OK=$((STEPS_OK+1)); } log_warn() { printf '%s[WARN]%s %s\n' "$C_YEL" "$C_NC" "$*"; } log_skip() { printf '%s[SKIP]%s %s\n' "$C_DIM" "$C_NC" "$*"; STEPS_SKIP=$((STEPS_SKIP+1)); } log_error() { printf '%s[ERROR]%s %s\n' "$C_RED" "$C_NC" "$*"; STEPS_FAIL=$((STEPS_FAIL+1)); } log_step() { printf '\n%s%s==> %s%s\n' "$C_BOLD" "$C_MAG" "$*" "$C_NC"; } trap_err() { # Safety net: logs unexpected failures (with line number) but never exits. log_warn "Unexpected error at line $1 (exit $2) — continuing." } report() { # report local sev="$1" st="$2" name="$3" detail="$4" REPORT_LINES+=("$sev|$st|$name|$detail") if [[ "$st" == "FAIL" ]]; then if [[ "$sev" == "CRITICAL" ]]; then CRITICAL_FAIL=$((CRITICAL_FAIL+1)); fi if [[ "$sev" == "OPTIONAL" ]]; then OPTIONAL_FAIL=$((OPTIONAL_FAIL+1)); fi fi } die() { log_error "$*"; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } is_systemd() { [[ -d /run/systemd/system ]]; } run_step() { # Fault-isolation wrapper: a section may fail; the script never stops. local name="$1" fn="$2" rc=0 log_step "$name" "$fn" rc=$? if [[ $rc -eq 0 ]]; then SECTION_RESULTS+=("PASS|$name") else SECTION_RESULTS+=("FAIL|$name") log_warn "Section '$name' reported failures (rc=$rc) — continuing with remaining sections." fi return 0 } # ---- usage ------------------------------------------------------------------ usage() { cat << 'USAGE' disable-ipv6.sh — reliably disable IPv6 on Debian-family Linux Usage: sudo bash disable-ipv6.sh Apply all changes and verify sudo bash disable-ipv6.sh --verify Re-check configs + runtime (post-reboot) sudo bash disable-ipv6.sh --help Layers applied (idempotent; each verified after writing): 1. Kernel cmdline ipv6.disable=1 (GRUB/grub.d, kernelstub, or systemd-boot) 2. sysctl.d drop-in + live sysctl 3. NetworkManager conf.d + existing connections (nmcli) 4. netplan (networkd renderer only) 5. UFW (/etc/default/ufw IPV6=no + /etc/ufw/sysctl.conf) 6. gai.conf IPv4 precedence 7. Avahi use-ipv6=no 8. ConnMan IPv6=false 9. Postfix inet_protocols=ipv4 10. modprobe blacklist + initramfs rebuild (only when changed) 11. ip6tables DROP policies (only if ip6tables already installed) A reboot is required for the kernel-level disable. Re-run with --verify after. USAGE } # ---- detection -------------------------------------------------------------- ID=""; ID_LIKE=""; VERSION_ID=""; PRETTY_NAME="" FAMILY="" # ubuntu | debian | unknown BOOTLOADER="" # kernelstub | systemd-boot | grub | unknown HAS_NM=0; HAS_NETPLAN=0; HAS_NETWORKD=0; HAS_UFW=0 HAS_AVAHI=0; HAS_IFUPDOWN=0; HAS_CONNMAN=0; SYSTEMD=0 load_os_release() { [[ -r /etc/os-release ]] || die "Cannot read /etc/os-release — unsupported system." # shellcheck disable=SC1091 . /etc/os-release ID="${ID:-unknown}" ID_LIKE="${ID_LIKE:-}" VERSION_ID="${VERSION_ID:-}" PRETTY_NAME="${PRETTY_NAME:-$ID $VERSION_ID}" } detect_family() { local like=" $ID $ID_LIKE " if [[ "$ID" =~ ^(ubuntu|pop|zorin)$ || "$like" == *" ubuntu "* ]]; then FAMILY="ubuntu" elif [[ "$ID" =~ ^(debian|mx)$ || "$like" == *" debian "* ]]; then FAMILY="debian" else FAMILY="unknown" fi } distro_supported_hint() { case "$ID" in pop) [[ "$VERSION_ID" == 22.04 || "$VERSION_ID" == 24.04* ]] && return 0 ;; zorin) [[ "$VERSION_ID" == 17* || "$VERSION_ID" == 18* ]] && return 0 ;; ubuntu) awk -v v="$VERSION_ID" 'BEGIN{exit !(v+0 >= 22.04)}' return $? ;; debian) [[ "$VERSION_ID" == 12* || "$VERSION_ID" == 13* ]] && return 0 ;; mx) return 0 ;; linuxmint) if [[ "$ID_LIKE" == *ubuntu* ]]; then awk -v v="$VERSION_ID" 'BEGIN{exit !(v+0 >= 22)}' return $? fi [[ "$VERSION_ID" == 6* || "$VERSION_ID" == 7* ]] && return 0 ;; esac return 1 } detect_container() { IN_CONTAINER=0 if have systemd-detect-virt && systemd-detect-virt --quiet --container; then IN_CONTAINER=1 elif [[ -f /.dockerenv || -f /run/.containerenv ]]; then IN_CONTAINER=1 fi return 0 } detect_bootloader() { if have kernelstub || [[ -f /etc/kernelstub/configuration ]]; then BOOTLOADER="kernelstub" elif [[ -d /boot/efi/loader/entries || -d /boot/loader/entries || -d /efi/loader/entries ]]; then if have bootctl && bootctl is-installed >/dev/null 2>&1; then BOOTLOADER="systemd-boot" elif [[ -f /etc/default/grub ]]; then BOOTLOADER="grub" else BOOTLOADER="systemd-boot" fi elif [[ -f /etc/default/grub ]]; then BOOTLOADER="grub" else BOOTLOADER="unknown" fi } detect_network() { if have nmcli || [[ -f /etc/NetworkManager/NetworkManager.conf ]]; then HAS_NM=1; fi if have netplan || [[ -d /etc/netplan ]]; then if compgen -G '/etc/netplan/*.yaml' >/dev/null 2>&1 || \ compgen -G '/etc/netplan/*.yml' >/dev/null 2>&1; then HAS_NETPLAN=1 fi fi if is_systemd && have systemctl && systemctl is-enabled systemd-networkd >/dev/null 2>&1; then HAS_NETWORKD=1 fi if have ufw || [[ -f /etc/default/ufw ]]; then HAS_UFW=1; fi if [[ -f /etc/avahi/avahi-daemon.conf ]]; then HAS_AVAHI=1; fi if [[ -f /etc/network/interfaces ]]; then HAS_IFUPDOWN=1; fi if have connmanctl || [[ -f /etc/connman/main.conf ]]; then HAS_CONNMAN=1; fi if is_systemd; then SYSTEMD=1; fi } print_detection() { log_step "System autodetection" log_info "OS: $PRETTY_NAME" log_info "ID / ID_LIKE: $ID / ${ID_LIKE:-none}" log_info "Family: $FAMILY" log_info "Bootloader: $BOOTLOADER" log_info "systemd: $([[ $SYSTEMD -eq 1 ]] && echo yes || echo no)" log_info "Container: $([[ $IN_CONTAINER -eq 1 ]] && echo yes || echo no)" log_info "NetworkManager:$([[ $HAS_NM -eq 1 ]] && echo yes || echo no)" log_info "netplan: $([[ $HAS_NETPLAN -eq 1 ]] && echo yes || echo no)" log_info "networkd: $([[ $HAS_NETWORKD -eq 1 ]] && echo yes || echo no)" log_info "UFW: $([[ $HAS_UFW -eq 1 ]] && echo yes || echo no)" log_info "Avahi: $([[ $HAS_AVAHI -eq 1 ]] && echo yes || echo no)" log_info "ifupdown: $([[ $HAS_IFUPDOWN -eq 1 ]] && echo yes || echo no)" log_info "ConnMan: $([[ $HAS_CONNMAN -eq 1 ]] && echo yes || echo no)" if distro_supported_hint; then log_ok "Distro/version is in the tested set." report CRITICAL PASS "Distro" "$PRETTY_NAME ($FAMILY / $BOOTLOADER)" else log_warn "Untested version ($PRETTY_NAME) — proceeding because it is Debian-family." report CRITICAL PASS "Distro" "$PRETTY_NAME (untested; debian-family)" fi if [[ $IN_CONTAINER -eq 1 ]]; then log_warn "Container detected: bootloader/initramfs steps will be skipped;" log_warn "IPv6 must ultimately be disabled on the container host." fi } preflight() { load_os_release detect_family [[ "$FAMILY" != "unknown" ]] || die "Not a Debian/Ubuntu-family system (ID=$ID ID_LIKE=$ID_LIKE)." detect_container detect_bootloader detect_network print_detection } # ---- file helpers ----------------------------------------------------------- backup_file() { local f="$1" [[ -n "$BACKUP_ROOT" && -f "$f" ]] || return 0 local dest="$BACKUP_ROOT${f}" mkdir -p "$(dirname "$dest")" || return 1 cp -a "$f" "$dest" || return 1 log_info "Backup: $f -> $dest" return 0 } write_file_idempotent() { # write_file_idempotent [line2 ...] # rc: 0 = written, 2 = already identical, 1 = error local dest="$1"; shift local tmp tmp="$(mktemp /tmp/disable-ipv6.XXXXXX)" || return 1 if ! printf '%s\n' "$@" > "$tmp"; then rm -f "$tmp" return 1 fi if [[ -f "$dest" ]] && cmp -s "$tmp" "$dest"; then rm -f "$tmp" return 2 fi mkdir -p "$(dirname "$dest")" || { rm -f "$tmp"; return 1; } [[ -f "$dest" ]] && backup_file "$dest" if mv "$tmp" "$dest"; then return 0 fi rm -f "$tmp" return 1 } note_written() { # note_written