doug445
(Will MacKinnon)
April 24, 2026, 11:18pm
1
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.
doug445
(Will MacKinnon)
April 25, 2026, 12:38pm
4
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!
doug445
(Will MacKinnon)
April 26, 2026, 7:37am
6
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