Fedora Asahi installer needs to include luks2 encryption

I have installed luks2 with argon2id myself, but the installer should have an option to do this at install time. It’s a glaring security oversight. They should also improve the internet security too. Great system now and my daily driver.

➜  ~ sudo cryptsetup luksDump /dev/nvme0n1p6                                          
LUKS header information
Version:       	2
Epoch:         	58503
Metadata area: 	16384 [bytes]
Keyslots area: 	16744448 [bytes]
UUID:          	b6aa6395-a3c1-4a87-8049-4f755c6c16e4
Label:         	(no label)
Subsystem:     	(no subsystem)
Flags:       	(no flags)

Data segments:
  0: crypt
	offset: 16777216 [bytes]
	length: (whole device)
	cipher: aes-xts-plain64
	sector: 512 [bytes]

Keyslots:
  0: luks2
	Key:        512 bits
	Priority:   normal
	Cipher:     aes-xts-plain64
	Cipher key: 512 bits
	PBKDF:      argon2id
	Time cost:  8
	Memory:     4194304
	Threads:    4
	Salt:       3f 83 df 62 52 07 88 e5 55 da 4e 51 5a 1a d4 50 
	            b9 bb c4 52 45 11 2c 6b 94 9e a7 14 bd 6e 46 81 
	AF stripes: 4000
	AF hash:    sha512
	Area offset:32768 [bytes]
	Area length:258048 [bytes]
	Digest ID:  0
Tokens:
Digests:
  0: pbkdf2
	Hash:       sha256
	Iterations: 586451
	Salt:       37 4c 63 ec 1a 4f 3a 00 16 f5 f5 b7 b4 12 3f d6 
	            a3 c1 bc c1 82 95 5b ea 80 3b d0 0f 59 ec 1d 30 
	Digest:     a1 b6 73 93 42 f8 3a 39 72 49 dd d8 a1 15 7b 7d 
	            64 49 0c 69 59 23 25 21 b2 d0 4f 3f 9a 81 58 1b 
➜  ~ lsblk -f                                                                         
NAME             FSTYPE      FSVER LABEL       UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
zram0            swap        1     zram0       ccd59fca-12c5-4ced-971d-985f499c686f                [SWAP]
nvme0n1                                                                                            
├─nvme0n1p1      apfs                          5ca785d8-5e24-4e28-8780-546ec6948cc5                
├─nvme0n1p2      apfs                          da30d635-3bad-416f-b9b1-7ae7598bc02a                
├─nvme0n1p3      apfs                          181aef52-dd99-40a5-b87f-08fd5b30b8b9                
├─nvme0n1p4      vfat        FAT32 EFI - FEDOR 5AD4-DF86                             366.9M    26% /boot/efi
├─nvme0n1p5      ext4        1.0   BOOT        9fb5e505-23c0-44ed-8afd-4c45eb960919  498.6M    42% /boot
├─nvme0n1p6      crypto_LUKS 2                 b6aa6395-a3c1-4a87-8049-4f755c6c16e4                
│ └─fedora_crypt btrfs             fedora      19215083-d0bc-45e5-ae3f-eebb4aa97385  369.1G    19% /home
│                                                                                                  /
└─nvme0n1p7      apfs                          266e2849-fcca-4823-a160-c6f51144f06f                
nvme0n2                                                                                            
nvme0n3                                                                                            
➜  ~ sudo dmsetup table --showkeys                                                    
fedora_crypt: 0 958185472 crypt aes-xts-plain64 :64:logon:cryptsetup:b6aa6395-a3c1-4a87-8049-4f755c6c16e4-d0 0 259:6 32768 1 allow_discards
➜  ~ netcheck                                                                         

━━━ FIREWALL ━━━
  Default zone: drop (all unsolicited inbound silently dropped)
  Log denied:   all
  Open services: none
  Open ports:    none

━━━ LISTENING PORTS ━━━
  ● :9050 127.0.0.1 (localhost only) 
  ● :2017 127.0.0.1 (localhost only) 
  ● :53 127.0.0.54 (localhost only) 
  ● :631 127.0.0.1 (localhost only) 
  ● :27500 0.0.0.0 (network exposed!) 
  ● :53 127.0.0.53%lo (localhost only) 
  ● :631 [::1] (localhost only) 
  ● :1716 * (network exposed!) 

━━━ WARP TUNNEL ━━━
  Status: Connected
  Tunnel Protocol: MASQUE (HTTP/3)
  Estimated latency: 25ms
  Estimated loss: 0.00%
  Exit check: warp=on (traffic is tunneled)
  Split-tunnel exclusions: 2 host(s) (run: warp-cli tunnel host list)

━━━ DNS ━━━
  Current server: 94.140.15.15#dns.adguard-dns.com
  ● DNSSEC: enforced
  ● DNS-over-TLS: on
  ● LLMNR: disabled
  ● mDNS: disabled

━━━ WI-FI PRIVACY (MAC randomization) ━━━
  Interface: wlp1s0f0
  Current MAC: be:27:4d:b4:81:63
  Hardware MAC: d3:35:b2:9f:7f:17
  Status: Randomized (current MAC differs from hardware)
  Config: /etc/NetworkManager/conf.d/mac-randomization.conf

━━━ ARP TABLE ━━━
  192.168.0.1	e4:b1:5f:da:5b:f8	TP-LINK TECHNOLOGIES CO.,LTD. (gateway)
  192.168.0.5	74:fe:ce:12:6a:b4	(Unknown)
  192.168.0.10	60:32:b1:72:5a:80	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.11	60:32:b1:72:5a:9e	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.12	60:32:b1:72:5a:84	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.13	60:32:b1:72:5a:a8	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.14	60:32:b1:72:5a:a6	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.15	60:32:b1:72:5a:7c	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.16	60:32:b1:72:5a:88	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.20	e4:c3:2a:db:26:40	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.127	da:96:ad:af:47:ca	(Unknown: locally administered)
  192.168.0.132	50:06:f5:02:be:1f	(Unknown)
  192.168.0.138	f0:f0:a4:b1:30:3b	Amazon Technologies Inc.
  192.168.0.143	52:30:37:1d:39:da	(Unknown: locally administered)
  192.168.0.159	54:2b:57:07:89:b5	Night Owl SP
  192.168.0.160	e6:e9:fa:50:26:30	(Unknown: locally administered)
  192.168.0.164	38:e3:9f:4d:98:5d	Motorola Mobility LLC, a Lenovo Company
  192.168.0.173	f4:dd:06:b9:04:aa	(Unknown)
  192.168.0.187	2c:9c:58:c0:9b:e4	(Unknown)
  192.168.0.205	00:17:6f:94:02:f2	PAX Computer Technology(Shenzhen) Ltd.
  192.168.0.210	5a:35:2f:3c:89:2c	(Unknown: locally administered)
  192.168.0.215	8c:70:5a:d3:12:e4	Intel Corporate
  192.168.0.226	28:7e:80:8e:d6:87	Hui Zhou Gaoshengda Technology Co.,LTD

━━━ RECENT FIREWALL DROPS (last 5 min) ━━━
  894 packets dropped
    192.168.0.173 → :15600 (UDP)
    192.168.0.173 → :15600 (UDP)
    192.168.0.173 → :15600 (UDP)
    192.168.0.173 → :15600 (UDP)
    192.168.0.143 → :57621 (UDP)

━━━ NETWATCH MONITOR ━━━
  Running
  278 events logged
  [2026-04-24 17:36:42] NetWatch started (pid=2300)
  [2026-04-24 17:41:34] NetWatch started (pid=2245)
  [2026-04-24 17:45:10] NetWatch started (pid=2221)

━━━ IP ADDRESSES ━━━
  ● wlp1s0f0         UP        192.168.0.102/24 fe80::aff1:d49f:a72b:61f9/64
  ● CloudflareWARP   UNKNOWN   172.16.0.2/32 2606:4700:110:8622:560a:82ed:3890:e76d/128 fe80::725a:3991:3a94:61c9/64

➜  ~ checkdns                             
Global
           Protocols: -LLMNR -mDNS +DNSOverTLS DNSSEC=yes/supported
    resolv.conf mode: stub
  Current DNS Server: 94.140.15.15 (AdGuard DNS)#dns.adguard-dns.com
         DNS Servers: 94.140.14.14 (AdGuard DNS)#dns.adguard-dns.com 94.140.15.15 (AdGuard DNS)#dns.adguard-dns.com
Fallback DNS Servers: 9.9.9.9 (Quad9)#dns.quad9.net 1.1.1.1 (Cloudflare)#cloudflare-dns.com

Link 2 (wlp1s0f0)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS +DNSOverTLS DNSSEC=yes/supported
     Default Route: no

Link 3 (CloudflareWARP)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS +DNSOverTLS DNSSEC=yes/supported
     Default Route: no

Cloudflare WARP
              Status: Connected
             Network: healthy

Avahi (mDNS)
      Current Status: active
             Startup: disabled

ECH Status
       DNS ECHConfig: yes (ech=AID+DQA8GwAgACALOY7S…)
      TLS ECH (curl): working
➜  ~ 

Hi, just a quick question, what are the commands (aliases?) netcheck and checkdns? Thank you.

netcheck

#!/usr/bin/env bash
# netcheck — one-shot network security status report
set -euo pipefail

BOLD='\033[1m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'

section() { echo -e "\n${BOLD}${CYAN}━━━ $1 ━━━${NC}"; }

# Firewall
section "FIREWALL"
zone=$(sudo firewall-cmd --get-default-zone 2>/dev/null || true)
log_denied=$(sudo firewall-cmd --get-log-denied 2>/dev/null || true)
if [[ "$zone" == "drop" ]]; then
    echo -e "  Default zone: ${GREEN}${zone}${NC} (all unsolicited inbound silently dropped)"
else
    echo -e "  Default zone: ${RED}${zone}${NC} (consider switching to 'drop')"
fi
echo -e "  Log denied:   ${log_denied}"
services=$(sudo firewall-cmd --list-services 2>/dev/null || true)
ports=$(sudo firewall-cmd --list-ports 2>/dev/null || true)
echo -e "  Open services: ${services:-${GREEN}none${NC}}"
echo -e "  Open ports:    ${ports:-${GREEN}none${NC}}"

# Listening ports
section "LISTENING PORTS"
ss -tlnp 2>/dev/null | tail -n +2 | while read -r _ _ _ local _ _ process; do
    addr="${local%:*}"
    port="${local##*:}"
    if [[ "$addr" == "127.0.0.1" ]] || [[ "$addr" == "[::1]" ]] || [[ "$addr" == "127.0.0.53%lo" ]] || [[ "$addr" == "127.0.0.54" ]]; then
        echo -e "  ${GREEN}●${NC} :${port} ${addr} (localhost only) ${process}"
    else
        echo -e "  ${RED}●${NC} :${port} ${addr} (network exposed!) ${process}"
    fi
done

# WARP status
section "WARP TUNNEL"
if command -v warp-cli &>/dev/null; then
    warp_status=$(warp-cli --accept-tos status 2>/dev/null | grep "Status update:" | awk '{print $3}' || true)
    if [[ "$warp_status" == "Connected" ]]; then
        echo -e "  Status: ${GREEN}Connected${NC}"
        warp-cli --accept-tos tunnel stats 2>/dev/null | grep -E "Protocol|latency|loss" | sed 's/^/  /'
        # Verify traffic actually flows through WARP
        warp_active=$(curl -s --max-time 3 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null | awk -F= '/^warp=/{print $2}' || true)
        if [[ "$warp_active" == "on" ]]; then
            echo -e "  Exit check: ${GREEN}warp=on${NC} (traffic is tunneled)"
        elif [[ -n "$warp_active" ]]; then
            echo -e "  Exit check: ${RED}warp=${warp_active}${NC} (tunnel not being used!)"
        fi
        split=$(warp-cli --accept-tos tunnel host list 2>/dev/null | tail -n +2 | grep -c . || true)
        (( split > 0 )) && echo -e "  Split-tunnel exclusions: ${YELLOW}${split} host(s)${NC} (run: warp-cli tunnel host list)"
    else
        echo -e "  Status: ${RED}${warp_status:-Unknown}${NC}"
    fi
else
    echo -e "  ${YELLOW}warp-cli not installed${NC} — install from https://pkg.cloudflareclient.com/"
fi

# DNS
section "DNS"
# Pull only the Global block from resolvectl (first Protocols line after Global:)
dns_server=$(resolvectl status 2>/dev/null | awk '/^Global/,/^Link/' | grep "Current DNS Server" | awk '{print $NF}' || true)
protos=$(resolvectl status 2>/dev/null | awk '/^Global/,/^Link/' | grep "Protocols" | head -1 | sed 's/^[[:space:]]*//' || true)
echo -e "  Current server: ${dns_server:-${RED}unknown${NC}}"
if [[ -n "$protos" ]]; then
    if [[ "$protos" == *"DNSSEC=yes"* ]]; then
        echo -e "  ${GREEN}●${NC} DNSSEC: enforced"
    else
        echo -e "  ${YELLOW}●${NC} DNSSEC: not strict"
    fi
    if [[ "$protos" == *"+DNSOverTLS"* ]]; then
        echo -e "  ${GREEN}●${NC} DNS-over-TLS: on"
    else
        echo -e "  ${YELLOW}●${NC} DNS-over-TLS: off"
    fi
    [[ "$protos" == *"-LLMNR"* ]]  && echo -e "  ${GREEN}●${NC} LLMNR: disabled"  || echo -e "  ${YELLOW}●${NC} LLMNR: enabled"
    [[ "$protos" == *"-mDNS"* ]]   && echo -e "  ${GREEN}●${NC} mDNS: disabled"   || echo -e "  ${YELLOW}●${NC} mDNS: enabled"
fi

# MAC randomization status
section "WI-FI PRIVACY (MAC randomization)"
wifi_iface=$(nmcli -t -f DEVICE,TYPE device 2>/dev/null | awk -F: '$2=="wifi"{print $1; exit}' || true)
if [[ -n "$wifi_iface" ]]; then
    current_mac=$(ip link show "$wifi_iface" 2>/dev/null | awk '/link\/ether/{print $2; exit}' || true)
    perm_mac=$(ip link show "$wifi_iface" 2>/dev/null | awk -F'permaddr ' 'NF>1{print $2; exit}' | awk '{print $1}' || true)
    echo -e "  Interface: ${wifi_iface}"
    echo -e "  Current MAC: ${BOLD}${current_mac}${NC}"
    [[ -n "$perm_mac" ]] && echo -e "  Hardware MAC: ${perm_mac}"

    if [[ -n "$perm_mac" ]] && [[ "$current_mac" != "$perm_mac" ]]; then
        echo -e "  Status: ${GREEN}Randomized${NC} (current MAC differs from hardware)"
    elif [[ -n "$current_mac" ]]; then
        # Check locally-administered bit as fallback
        first_octet=$(echo "$current_mac" | cut -d: -f1)
        first_dec=$((16#${first_octet}))
        if (( first_dec & 0x02 )); then
            echo -e "  Status: ${GREEN}Locally administered${NC} (MAC is random)"
        else
            echo -e "  Status: ${RED}Hardware MAC exposed${NC}"
        fi
    fi

    # Show NM config
    nm_conf="/etc/NetworkManager/conf.d/mac-randomization.conf"
    if [[ -f "$nm_conf" ]]; then
        echo -e "  Config: ${GREEN}${nm_conf}${NC}"
    fi
fi

# ARP table — prefer arp-scan (active scan + vendor lookup) over passive ip neigh
section "ARP TABLE"
gateway_ip=$(ip route show default 2>/dev/null | awk '/default/{print $3; exit}' || true)

arp_output=""
if command -v arp-scan &>/dev/null; then
    arp_output=$(sudo arp-scan --localnet --ignoredups 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -V || true)
fi

if [[ -n "$arp_output" ]]; then
    while IFS=$'\t' read -r ip mac vendor; do
        if [[ "$ip" == "$gateway_ip" ]]; then
            echo -e "  ${BOLD}${ip}${NC}\t${mac}\t${vendor} ${YELLOW}(gateway)${NC}"
        else
            echo -e "  ${ip}\t${mac}\t${vendor}"
        fi
    done <<< "$arp_output"
else
    # Fallback: passive kernel neighbor table
    ip neigh show 2>/dev/null | while read -r ip _ _ _ mac state; do
        if [[ "$ip" == "$gateway_ip" ]]; then
            echo -e "  ${BOLD}${ip}${NC} ${mac} (gateway) [${state}]"
        else
            echo -e "  ${ip} ${mac} [${state}]"
        fi
    done
fi

# Recent drops
section "RECENT FIREWALL DROPS (last 5 min)"
drops=$(journalctl -k --since "5 min ago" --no-pager 2>/dev/null | grep -c "_DROP" || true)
if (( drops > 0 )); then
    echo -e "  ${YELLOW}${drops} packets dropped${NC}"
    journalctl -k --since "5 min ago" --no-pager 2>/dev/null | grep "_DROP" | grep -v "DST=255.255.255.255" | grep -v "DST=224." | grep -v "DPT=5353" | tail -5 | while read -r line; do
        src=$(echo "$line" | grep -oP 'SRC=\K[0-9.]+' || echo "?")
        proto=$(echo "$line" | grep -oP 'PROTO=\K\w+' || echo "?")
        dpt=$(echo "$line" | grep -oP 'DPT=\K\d+' || echo "?")
        echo -e "    ${src} → :${dpt} (${proto})"
    done
else
    echo -e "  ${GREEN}No drops — quiet network${NC}"
fi

# NetWatch service
section "NETWATCH MONITOR"
if systemctl --user is-active netwatch.service &>/dev/null; then
    echo -e "  ${GREEN}Running${NC}"
    if [[ -f "${HOME}/.local/share/netwatch/events.log" ]]; then
        events=$(wc -l < "${HOME}/.local/share/netwatch/events.log")
        echo -e "  ${events} events logged"
        tail -3 "${HOME}/.local/share/netwatch/events.log" | sed 's/^/  /'
    fi
else
    echo -e "  ${RED}Not running${NC} — start with: systemctl --user start netwatch"
fi

# IP addresses
section "IP ADDRESSES"
ip -brief addr 2>/dev/null | while read -r iface state addrs; do
    [[ "$iface" == "lo" ]] && continue
    case "$state" in
        UP)      color="${GREEN}" ;;
        DOWN)    color="${RED}" ;;
        *)       color="${YELLOW}" ;;
    esac
    printf "  ${color}●${NC} ${BOLD}%-16s${NC} ${color}%-8s${NC}  %s\n" "$iface" "$state" "$addrs"
done

echo ""

checkdns (put in function section of your .zshrc)

unalias checkdns 2>/dev/null
checkdns() {
    resolvectl status | sed \
        -e 's/94\.140\.14\.14/94.140.14.14 (AdGuard DNS)/g' \
        -e 's/94\.140\.15\.15/94.140.15.15 (AdGuard DNS)/g' \
        -e 's/2a10:50c0::ad1:ff/2a10:50c0::ad1:ff (AdGuard DNS)/g' \
        -e 's/2a10:50c0::ad2:ff/2a10:50c0::ad2:ff (AdGuard DNS)/g' \
        -e 's/1\.1\.1\.1/1.1.1.1 (Cloudflare)/g' \
        -e 's/1\.0\.0\.1/1.0.0.1 (Cloudflare)/g' \
        -e 's/9\.9\.9\.9/9.9.9.9 (Quad9)/g' \
        -e 's/149\.112\.112\.112/149.112.112.112 (Quad9)/g'
    # Cloudflare WARP status    
    printf "\n\e[1mCloudflare WARP\e[0m\n"
    local warp_status warp_network
    warp_status=$(warp-cli status 2>/dev/null | grep -oP 'Status update: \K.*')
    warp_network=$(warp-cli status 2>/dev/null | grep -oP 'Network: \K.*')
    if [[ -n "$warp_status" ]]; then
        printf "\e[0;94m%21s\e[0m %s\n" "Status:" "$warp_status"
        printf "\e[0;94m%21s\e[0m %s\n" "Network:" "$warp_network"
    else
        printf "\e[0;94m%21s\e[0m \e[0;31mnot running\e[0m\n" "Status:"
    fi
    
    printf "\n\e[1mAvahi (mDNS)\e[0m\n\e[0;94m%21s\e[0m %s\n\e[0;94m%21s\e[0m %s\n" \
    "Current Status:" "$(systemctl is-active avahi-daemon 2>/dev/null)" \
    "Startup:" "$(systemctl is-enabled avahi-daemon 2>/dev/null)"
    
    # ECH status
    local domain="defo.ie"
    printf "\n\e[1mECH Status\e[0m\n"

    # DNS layer: HTTPS record carries ECHConfig
    local https_rec echconfig
    https_rec=$(dig +short HTTPS "$domain" 2>/dev/null)
    echconfig=$(echo "$https_rec" | grep -o 'ech=[A-Za-z0-9+/=]*' | head -1)
    
    if [[ -n "$echconfig" ]]; then
        printf "\e[0;94m%21s\e[0m \e[0;32myes\e[0m (%s…)\n" "DNS ECHConfig:" "${echconfig:0:24}"
    elif [[ -n "$https_rec" ]]; then
        printf "\e[0;94m%21s\e[0m \e[0;33mHTTPS record found, no ECH\e[0m\n" "DNS ECHConfig:"
    else
        printf "\e[0;94m%21s\e[0m \e[0;31mno HTTPS record returned\e[0m\n" "DNS ECHConfig:"
    fi

    # TLS layer: curl --ech hard (no cleartext-SNI fallback)
    local curl_err
    curl_err=$(\curl --ech hard --doh-url https://dns.cloudflare.com/dns-query -fso /dev/null "https://$domain/" 2>&1)
    local curl_exit=$?
    
    if [[ $curl_exit -eq 0 ]]; then
        printf "\e[0;94m%21s\e[0m \e[0;32mworking\e[0m\n" "TLS ECH (curl):"
    elif echo "$curl_err" | grep -qi "unknown\|unrecognized\|invalid option"; then
        printf "\e[0;94m%21s\e[0m \e[0;33mcurl lacks ECH support\e[0m\n" "TLS ECH (curl):"
    else
        printf "\e[0;94m%21s\e[0m \e[0;31mfailed\e[0m (verify in browser: defo.ie/ech-check.php)\n" "TLS ECH (curl):"
    fi
}

2 Likes

Great, thanks for sharing!

I have updated netcheck to show top drop sources so you can see in real time top offenders.

#!/usr/bin/env bash
# netcheck — one-shot network security status report
set -euo pipefail

BOLD='\033[1m'
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'

section() { echo -e "\n${BOLD}${CYAN}━━━ $1 ━━━${NC}"; }

# Firewall
section "FIREWALL"
zone=$(sudo firewall-cmd --get-default-zone 2>/dev/null || true)
log_denied=$(sudo firewall-cmd --get-log-denied 2>/dev/null || true)
if [[ "$zone" == "drop" ]]; then
    echo -e "  Default zone: ${GREEN}${zone}${NC} (all unsolicited inbound silently dropped)"
else
    echo -e "  Default zone: ${RED}${zone}${NC} (consider switching to 'drop')"
fi
echo -e "  Log denied:   ${log_denied}"
services=$(sudo firewall-cmd --list-services 2>/dev/null || true)
ports=$(sudo firewall-cmd --list-ports 2>/dev/null || true)
echo -e "  Open services: ${services:-${GREEN}none${NC}}"
echo -e "  Open ports:    ${ports:-${GREEN}none${NC}}"

# Listening ports
section "LISTENING PORTS"
ss -tlnp 2>/dev/null | tail -n +2 | while read -r _ _ _ local _ _ process; do
    addr="${local%:*}"
    port="${local##*:}"
    if [[ "$addr" == "127.0.0.1" ]] || [[ "$addr" == "[::1]" ]] || [[ "$addr" == "127.0.0.53%lo" ]] || [[ "$addr" == "127.0.0.54" ]]; then
        echo -e "  ${GREEN}●${NC} :${port} ${addr} (localhost only) ${process}"
    else
        echo -e "  ${RED}●${NC} :${port} ${addr} (network exposed!) ${process}"
    fi
done

# WARP status
section "WARP TUNNEL"
if command -v warp-cli &>/dev/null; then
    warp_status=$(warp-cli --accept-tos status 2>/dev/null | grep "Status update:" | awk '{print $3}' || true)
    if [[ "$warp_status" == "Connected" ]]; then
        echo -e "  Status: ${GREEN}Connected${NC}"
        warp-cli --accept-tos tunnel stats 2>/dev/null | grep -E "Protocol|latency|loss" | sed 's/^/  /'
        # Verify traffic actually flows through WARP
        warp_active=$(curl -s --max-time 3 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null | awk -F= '/^warp=/{print $2}' || true)
        if [[ "$warp_active" == "on" ]]; then
            echo -e "  Exit check: ${GREEN}warp=on${NC} (traffic is tunneled)"
        elif [[ -n "$warp_active" ]]; then
            echo -e "  Exit check: ${RED}warp=${warp_active}${NC} (tunnel not being used!)"
        fi
        split=$(warp-cli --accept-tos tunnel host list 2>/dev/null | tail -n +2 | grep -c . || true)
        (( split > 0 )) && echo -e "  Split-tunnel exclusions: ${YELLOW}${split} host(s)${NC} (run: warp-cli tunnel host list)"
    else
        echo -e "  Status: ${RED}${warp_status:-Unknown}${NC}"
    fi
else
    echo -e "  ${YELLOW}warp-cli not installed${NC} — install from https://pkg.cloudflareclient.com/"
fi

# DNS
section "DNS"
# Pull only the Global block from resolvectl (first Protocols line after Global:)
dns_server=$(resolvectl status 2>/dev/null | awk '/^Global/,/^Link/' | grep "Current DNS Server" | awk '{print $NF}' || true)
protos=$(resolvectl status 2>/dev/null | awk '/^Global/,/^Link/' | grep "Protocols" | head -1 | sed 's/^[[:space:]]*//' || true)
echo -e "  Current server: ${dns_server:-${RED}unknown${NC}}"
if [[ -n "$protos" ]]; then
    if [[ "$protos" == *"DNSSEC=yes"* ]]; then
        echo -e "  ${GREEN}●${NC} DNSSEC: enforced"
    else
        echo -e "  ${YELLOW}●${NC} DNSSEC: not strict"
    fi
    if [[ "$protos" == *"+DNSOverTLS"* ]]; then
        echo -e "  ${GREEN}●${NC} DNS-over-TLS: on"
    else
        echo -e "  ${YELLOW}●${NC} DNS-over-TLS: off"
    fi
    [[ "$protos" == *"-LLMNR"* ]]  && echo -e "  ${GREEN}●${NC} LLMNR: disabled"  || echo -e "  ${YELLOW}●${NC} LLMNR: enabled"
    [[ "$protos" == *"-mDNS"* ]]   && echo -e "  ${GREEN}●${NC} mDNS: disabled"   || echo -e "  ${YELLOW}●${NC} mDNS: enabled"
fi

# MAC randomization status
section "WI-FI PRIVACY (MAC randomization)"
wifi_iface=$(nmcli -t -f DEVICE,TYPE device 2>/dev/null | awk -F: '$2=="wifi"{print $1; exit}' || true)
if [[ -n "$wifi_iface" ]]; then
    current_mac=$(ip link show "$wifi_iface" 2>/dev/null | awk '/link\/ether/{print $2; exit}' || true)
    perm_mac=$(ip link show "$wifi_iface" 2>/dev/null | awk -F'permaddr ' 'NF>1{print $2; exit}' | awk '{print $1}' || true)
    echo -e "  Interface: ${wifi_iface}"
    echo -e "  Current MAC: ${BOLD}${current_mac}${NC}"
    [[ -n "$perm_mac" ]] && echo -e "  Hardware MAC: ${perm_mac}"

    if [[ -n "$perm_mac" ]] && [[ "$current_mac" != "$perm_mac" ]]; then
        echo -e "  Status: ${GREEN}Randomized${NC} (current MAC differs from hardware)"
    elif [[ -n "$current_mac" ]]; then
        # Check locally-administered bit as fallback
        first_octet=$(echo "$current_mac" | cut -d: -f1)
        first_dec=$((16#${first_octet}))
        if (( first_dec & 0x02 )); then
            echo -e "  Status: ${GREEN}Locally administered${NC} (MAC is random)"
        else
            echo -e "  Status: ${RED}Hardware MAC exposed${NC}"
        fi
    fi

    # Show NM config
    nm_conf="/etc/NetworkManager/conf.d/mac-randomization.conf"
    if [[ -f "$nm_conf" ]]; then
        echo -e "  Config: ${GREEN}${nm_conf}${NC}"
    fi
fi

# ARP table — prefer arp-scan (active scan + vendor lookup) over passive ip neigh
section "ARP TABLE"
gateway_ip=$(ip route show default 2>/dev/null | awk '/default/{print $3; exit}' || true)

arp_output=""
if command -v arp-scan &>/dev/null; then
    arp_output=$(sudo arp-scan --localnet --ignoredups 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | sort -V || true)
fi

if [[ -n "$arp_output" ]]; then
    while IFS=$'\t' read -r ip mac vendor; do
        if [[ "$ip" == "$gateway_ip" ]]; then
            echo -e "  ${BOLD}${ip}${NC}\t${mac}\t${vendor} ${YELLOW}(gateway)${NC}"
        else
            echo -e "  ${ip}\t${mac}\t${vendor}"
        fi
    done <<< "$arp_output"
else
    # Fallback: passive kernel neighbor table
    ip neigh show 2>/dev/null | while read -r ip _ _ _ mac state; do
        if [[ "$ip" == "$gateway_ip" ]]; then
            echo -e "  ${BOLD}${ip}${NC} ${mac} (gateway) [${state}]"
        else
            echo -e "  ${ip} ${mac} [${state}]"
        fi
    done
fi

# Recent drops
section "RECENT FIREWALL DROPS (last 5 min)"
drops=$(journalctl -k --since "5 min ago" --no-pager 2>/dev/null | grep -c "_DROP" || true)
if (( drops > 0 )); then
    echo -e "  ${YELLOW}${drops} packets dropped${NC}"
    journalctl -k --since "5 min ago" --no-pager 2>/dev/null | grep "_DROP" | grep -v "DST=255.255.255.255" | grep -v "DST=224." | grep -v "DPT=5353" | tail -5 | while read -r line; do
        src=$(echo "$line" | grep -oP 'SRC=\K[0-9a-fA-F.:]+' || echo "?")
        proto=$(echo "$line" | grep -oP 'PROTO=\K\w+' || echo "?")
        dpt=$(echo "$line" | grep -oP 'DPT=\K\d+' || echo "?")
        echo -e "    ${src} → :${dpt} (${proto})"
    done
else
    echo -e "  ${GREEN}No drops — quiet network${NC}"
fi

# Top drop sources (from netwatch log, with MAC+vendor)
section "TOP DROP SOURCES (netwatch log)"
NW_LOG="${HOME}/.local/share/netwatch/events.log"
OUI=/usr/share/hwdata/oui.txt
if [[ -r "$NW_LOG" ]] && grep -q DROP "$NW_LOG" 2>/dev/null; then
    declare -A lan_mac lan_vendor
    if command -v arp-scan &>/dev/null; then
        while IFS=$'\t' read -r sip smac svendor; do
            [[ -n "$sip" ]] || continue
            lan_mac[$sip]="$smac"
            lan_vendor[$sip]="$svendor"
        done < <(sudo arp-scan --localnet --ignoredups 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || true)
    fi
    grep DROP "$NW_LOG" | awk -F'src=' 'NF>1{print $2}' | awk '{print $1}' | sort | uniq -c | sort -rn | head -10 | while read -r count ip; do
        mac="${lan_mac[$ip]:-}"
        vendor="${lan_vendor[$ip]:-}"
        if [[ -z "$mac" ]]; then
            mac=$(ip neigh show "$ip" 2>/dev/null | grep -oP 'lladdr \K[a-f0-9:]+' | head -1 || true)
        fi
        if [[ -z "$vendor" && -n "$mac" && -r "$OUI" ]]; then
            prefix=$(echo "${mac:0:8}" | tr -d : | tr 'a-f' 'A-F')
            vendor=$(grep "^$prefix" "$OUI" 2>/dev/null | head -1 | sed 's/^[0-9A-F]*\s*(base 16)\s*//' | tr -d '\r' || true)
        fi
        host=$(getent hosts "$ip" 2>/dev/null | awk '{print $2}' || true)
        printf "  %6s  %-15s  %-18s  %-28s  %s\n" "$count" "$ip" "${mac:--}" "${vendor:--}" "${host:-}"
    done
else
    echo -e "  ${GREEN}No drops in netwatch log${NC}"
fi

# NetWatch service
section "NETWATCH MONITOR"
if systemctl --user is-active netwatch.service &>/dev/null; then
    echo -e "  ${GREEN}Running${NC}"
    if [[ -f "${HOME}/.local/share/netwatch/events.log" ]]; then
        events=$(wc -l < "${HOME}/.local/share/netwatch/events.log")
        echo -e "  ${events} events logged"
        tail -3 "${HOME}/.local/share/netwatch/events.log" | sed 's/^/  /'
    fi
else
    echo -e "  ${RED}Not running${NC} — start with: systemctl --user start netwatch"
fi

# IP addresses
section "IP ADDRESSES"
ip -brief addr 2>/dev/null | while read -r iface state addrs; do
    [[ "$iface" == "lo" ]] && continue
    case "$state" in
        UP)      color="${GREEN}" ;;
        DOWN)    color="${RED}" ;;
        *)       color="${YELLOW}" ;;
    esac
    printf "  ${color}●${NC} ${BOLD}%-16s${NC} ${color}%-8s${NC}  %s\n" "$iface" "$state" "$addrs"
done

echo ""

sample output now


━━━ FIREWALL ━━━
  Default zone: drop (all unsolicited inbound silently dropped)
  Log denied:   all
  Open services: none
  Open ports:    none

━━━ LISTENING PORTS ━━━
  ● :9050 127.0.0.1 (localhost only) 
  ● :53 127.0.0.53%lo (localhost only) 
  ● :53 127.0.0.54 (localhost only) 
  ● :631 127.0.0.1 (localhost only) 
  ● :2017 127.0.0.1 (localhost only) 
  ● :1716 * (network exposed!) 
  ● :631 [::1] (localhost only) 

━━━ WARP TUNNEL ━━━
  Status: Connected
  Tunnel Protocol: MASQUE (HTTP/2)
  Estimated latency: 31ms
  Estimated loss: 1.89%
  Exit check: warp=on (traffic is tunneled)
  Split-tunnel exclusions: 2 host(s) (run: warp-cli tunnel host list)

━━━ DNS ━━━
  Current server: 94.140.14.14#dns.adguard-dns.com
  ● DNSSEC: enforced
  ● DNS-over-TLS: on
  ● LLMNR: disabled
  ● mDNS: disabled

━━━ WI-FI PRIVACY (MAC randomization) ━━━
  Interface: wlp1s0f0
  Current MAC: 0e:15:6c:38:55:06
  Hardware MAC: b3:23:c5:55:7f:17
  Status: Randomized (current MAC differs from hardware)
  Config: /etc/NetworkManager/conf.d/mac-randomization.conf

━━━ ARP TABLE ━━━
  192.168.0.1	e4:c3:2a:da:5b:f8	TP-LINK TECHNOLOGIES CO.,LTD. (gateway)
  192.168.0.5	74:fe:ce:12:6a:b4	(Unknown)
  192.168.0.10	60:32:b1:72:5a:80	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.11	60:32:b1:72:5a:9e	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.12	60:32:b1:72:5a:84	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.13	60:32:b1:72:5a:a8	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.14	60:32:b1:72:5a:a6	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.15	60:32:b1:72:5a:7c	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.16	60:32:b1:72:5a:88	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.20	e4:c3:2a:db:26:40	TP-LINK TECHNOLOGIES CO.,LTD.
  192.168.0.102	5e:60:ea:47:f8:15	(Unknown: locally administered)
  192.168.0.132	50:06:f5:02:be:1f	(Unknown)
  192.168.0.137	c4:d9:87:84:aa:08	Intel Corporate
  192.168.0.138	f0:f0:a4:b1:30:3b	Amazon Technologies Inc.
  192.168.0.147	da:96:ad:af:47:ca	(Unknown: locally administered)
  192.168.0.159	54:2b:57:07:89:b5	Night Owl SP
  192.168.0.165	72:f9:b1:cf:b0:42	(Unknown: locally administered)
  192.168.0.173	f4:dd:06:b9:04:aa	(Unknown)
  192.168.0.187	2c:9c:58:c0:9b:e4	(Unknown)
  192.168.0.213	86:0b:23:42:7d:af	(Unknown: locally administered)
  192.168.0.215	8c:70:5a:d3:12:e4	Intel Corporate
  192.168.0.216	06:43:8a:3d:24:b1	(Unknown: locally administered)
  192.168.0.217	00:17:6f:94:02:f2	PAX Computer Technology(Shenzhen) Ltd.
  192.168.0.226	28:7e:80:8e:d6:87	Hui Zhou Gaoshengda Technology Co.,LTD
  192.168.0.246	e2:9e:19:6d:1f:86	(Unknown: locally administered)

━━━ RECENT FIREWALL DROPS (last 5 min) ━━━
  122 packets dropped
    192.168.0.1 → :137 (UDP)
    192.168.0.1 → :137 (UDP)
    192.168.0.1 → :137 (UDP)
    192.168.0.1 → :137 (UDP)

━━━ TOP DROP SOURCES (netwatch log) ━━━
     336  192.168.0.138    f0:f0:a4:b1:30:3b   Amazon Technologies Inc.      
      87  192.168.0.1      e4:c3:2a:da:5b:f8   TP-LINK TECHNOLOGIES CO.,LTD.  _gateway
      69  192.168.0.173    f4:dd:06:b9:04:aa   (Unknown)                     
      25  162.159.198.2    -                   -                             
       2  194.14.0.242     -                   -                             194-14-0-242.cust.srstubes.net
       2  192.168.0.143    -                   -                             
       1  94.140.15.15     -                   -                             dns.adguard-dns.com
       1  94.140.14.14     -                   -                             dns.adguard-dns.com
       1  87.106.155.25    -                   -                             ip87-106-155-25.pbiaas.com
       1  46.34.54.34      -                   -                             

━━━ NETWATCH MONITOR ━━━
  Running
  676 events logged
  [2026-04-26 02:09:49] SUMMARY drops=1 unique_ips=1
  [2026-04-26 03:19:40] NetWatch started (pid=514633)
  [2026-04-26 03:23:03] NetWatch started (pid=523247)

━━━ IP ADDRESSES ━━━
  ● wlp1s0f0         UP        192.168.0.149/24 fe80::aff1:b51f:a72b:52f9/64
  ● CloudflareWARP   UNKNOWN   172.16.0.2/32 2606:4700:110:8622:560a:82ed:3890:e76d/128 fe80::bae0:da0a:46b3:6fe5/64

➜  ~ 

you also need netwatch to make this all work.

netwatch

#!/usr/bin/env bash
# netwatch — monitor for network probes/attacks, send desktop notifications
# Watches firewalld dropped packets + new inbound connections + ARP anomalies

set -euo pipefail

LOG_DIR="${HOME}/.local/share/netwatch"
LOG_FILE="${LOG_DIR}/events.log"
POLL_INTERVAL=10
ALERT_THRESHOLD=5          # notify after this many drops from same IP in window
ALERT_WINDOW=60            # seconds
SUMMARY_INTERVAL=300       # summary notification every 5 min if activity

mkdir -p "$LOG_DIR"

# Resolve DBUS for notify-send from a systemd service
if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
    export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus"
fi

notify() {
    local urgency="$1" title="$2" body="$3"
    # KDE pins critical notifications regardless of -t; downgrade to normal
    # and prepend marker to title so alerts still look urgent.
    local icon="network-error"
    if [[ "$urgency" == "critical" ]]; then
        urgency="normal"
        title="⚠ $title"
        icon="security-low"
    fi
    # --hint=string:desktop-entry:netwatch — so KDE Plasma keeps entries in the
    # notification history (bell icon). Without it, Plasma 6 treats the app as
    # unknown and discards notifications after the popup timeout.
    notify-send -u "$urgency" -t 15000 -i "$icon" -a "NetWatch" \
        --hint=string:desktop-entry:netwatch \
        "$title" "$body" 2>/dev/null || true
}

log_event() {
    local ts
    ts=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$ts] $1" >> "$LOG_FILE"
}

# Track per-IP drop counts within the alert window
declare -A ip_counts
declare -A ip_first_seen
declare -A ip_alerted
last_summary=$(date +%s)
last_check=$last_summary
total_drops=0
unique_ips=0

reset_counters() {
    ip_counts=()
    ip_first_seen=()
    ip_alerted=()
    total_drops=0
    unique_ips=0
}

# Filter out broadcast/discovery noise that isn't a real probe
is_noise() {
    local src="$1" dst="$2" dpt="$3"
    [[ "$src" == "unknown" || "$src" == 127.* || "$src" == "::1" || "$src" == fe80:* ]] && return 0
    [[ "$dst" == 255.255.255.255 || "$dst" == 224.* || "$dst" == *.255 ]] && return 0
    [[ "$dst" == ff[0-9a-f][0-9a-f]:* || "$dst" == fe80:* ]] && return 0
    case "$dpt" in
        5353)  return 0 ;;  # mDNS
        1900)  return 0 ;;  # SSDP/UPnP
        137)   return 0 ;;  # NetBIOS name service
        138)   return 0 ;;  # NetBIOS datagram
        15600) return 0 ;;  # Samsung Smart View
        15500) return 0 ;;  # LG Smart Share
        3702)  return 0 ;;  # WS-Discovery
    esac
    return 1
}

process_drop() {
    local src_ip="$1" proto="$2" dpt="$3"
    local now
    now=$(date +%s)

    # Expire old entries outside the alert window
    for ip in "${!ip_first_seen[@]}"; do
        if (( now - ip_first_seen[$ip] > ALERT_WINDOW )); then
            unset "ip_counts[$ip]" "ip_first_seen[$ip]" "ip_alerted[$ip]"
        fi
    done

    # Count this drop
    if [[ -z "${ip_counts[$src_ip]:-}" ]]; then
        ip_counts[$src_ip]=0
        ip_first_seen[$src_ip]=$now
        (( unique_ips++ )) || true
    fi
    (( ip_counts[$src_ip]++ )) || true
    (( total_drops++ )) || true

    log_event "DROP src=$src_ip proto=$proto dpt=$dpt"

    # Alert if threshold crossed for this IP
    if (( ip_counts[$src_ip] >= ALERT_THRESHOLD )) && [[ -z "${ip_alerted[$src_ip]:-}" ]]; then
        ip_alerted[$src_ip]=1
        notify critical "Port Scan Detected" "Source: $src_ip\n${ip_counts[$src_ip]} probes in ${ALERT_WINDOW}s (${proto})"
        log_event "ALERT port_scan src=$src_ip count=${ip_counts[$src_ip]}"
    fi
}

check_arp_spoofing() {
    local gateway_ip gateway_mac
    gateway_ip=$(ip route show default | awk '/default/{print $3; exit}')
    [[ -z "$gateway_ip" ]] && return

    gateway_mac=$(ip neigh show "$gateway_ip" 2>/dev/null | awk '{print $5; exit}')
    [[ -z "$gateway_mac" ]] && return

    local stored_mac
    stored_mac=$(cat "$LOG_DIR/gateway_mac" 2>/dev/null || true)

    if [[ -z "$stored_mac" ]]; then
        echo "$gateway_mac" > "$LOG_DIR/gateway_mac"
        echo "$gateway_ip" > "$LOG_DIR/gateway_ip"
        log_event "ARP gateway=$gateway_ip mac=$gateway_mac (stored)"
    elif [[ "$stored_mac" != "$gateway_mac" ]]; then
        notify critical "ARP Spoofing Detected!" "Gateway $gateway_ip MAC changed!\nWas: $stored_mac\nNow: $gateway_mac\n\nSomeone may be intercepting your traffic!"
        log_event "ALERT arp_spoof gateway=$gateway_ip old=$stored_mac new=$gateway_mac"
        echo "$gateway_mac" > "$LOG_DIR/gateway_mac"
    fi
}

check_new_network() {
    local current_ssid
    current_ssid=$(nmcli -t -f active,ssid dev wifi 2>/dev/null | grep '^yes:' | cut -d: -f2- || true)
    local stored_ssid
    stored_ssid=$(cat "$LOG_DIR/current_ssid" 2>/dev/null || true)

    if [[ "$current_ssid" != "$stored_ssid" ]] && [[ -n "$current_ssid" ]]; then
        echo "$current_ssid" > "$LOG_DIR/current_ssid"
        # Reset gateway MAC when network changes
        rm -f "$LOG_DIR/gateway_mac"
        log_event "NETWORK changed to '$current_ssid'"
        notify normal "Network Changed" "Connected to: $current_ssid\nMonitoring started."
    fi
}

# ── Startup ─────────────────────────────────────────────────────────────
log_event "NetWatch started (pid=$$)"
notify normal "NetWatch Active" "Monitoring for network probes and attacks."
check_new_network
check_arp_spoofing

# ── Main event loop ─────────────────────────────────────────────────────
# Single-process design: process substitution keeps the while loop in this
# shell so counters and associative arrays are shared — no subshell isolation.
exec 3< <(journalctl -k -f --no-pager -o short 2>/dev/null)

while true; do
    if read -r -t "$POLL_INTERVAL" line <&3; then
        if [[ "$line" == *"_DROP"* ]] || [[ "$line" == *"FINAL_REJECT"* ]]; then
            src_ip=$(echo "$line" | grep -oP 'SRC=\K[0-9a-fA-F.:]+' || echo "unknown")
            proto=$(echo "$line" | grep -oP 'PROTO=\K\w+' || echo "?")
            dpt=$(echo "$line" | grep -oP 'DPT=\K\d+' || echo "?")
            dst_ip=$(echo "$line" | grep -oP 'DST=\K[0-9a-fA-F.:]+' || echo "")

            if ! is_noise "$src_ip" "$dst_ip" "$dpt"; then
                process_drop "$src_ip" "$proto" "$dpt"
            fi
        fi
    fi

    # Periodic checks
    now=$(date +%s)
    if (( now - last_check >= POLL_INTERVAL )); then
        check_arp_spoofing
        check_new_network
        last_check=$now
    fi

    # Periodic summary
    if (( total_drops > 0 && now - last_summary >= SUMMARY_INTERVAL )); then
        notify low "NetWatch Summary" "${total_drops} dropped packets from ${unique_ips} source(s) in last ${SUMMARY_INTERVAL}s"
        log_event "SUMMARY drops=$total_drops unique_ips=$unique_ips"
        last_summary=$now
        reset_counters
    fi
done
2 Likes