wgctl/commands/list.command.sh
2026-05-06 23:02:12 +00:00

427 lines
No EOL
10 KiB
Bash

#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::list::on_load() {
flag::register --type
flag::register --online
flag::register --offline
flag::register --restricted
flag::register --blocked
flag::register --allowed
flag::register --detailed
flag::register --name
}
# ============================================
# Help
# ============================================
function cmd::list::help() {
cat <<EOF
Usage: wgctl list [options]
List all WireGuard clients.
Options:
--type <type> Filter by device type
--online Show only connected clients
--offline Show only disconnected clients
--allowed Show only fully allowed clients
--restricted Show only restricted clients
--blocked Show only blocked clients
--detailed Show full detail cards for all clients
--name <name> Show detail card for a single client
Examples:
wgctl list
wgctl ls --type phone
wgctl list --online
wgctl list --blocked
wgctl list --allowed
wgctl list --restricted
wgctl list --detailed
wgctl list --name phone-nuno
EOF
}
# ============================================
# Status Helpers
# ============================================
function cmd::list::last_handshake_ts() {
local public_key="$1"
wg show "$(config::interface)" latest-handshakes 2>/dev/null \
| grep "^${public_key}" \
| awk '{print $2}'
}
function cmd::list::last_dropped_ts() {
local client_ip="$1"
journalctl -k --grep "wgctl-dropped: " 2>/dev/null \
| grep "SRC=${client_ip}" \
| tail -1 \
| awk '{print $1, $2, $3}'
}
function cmd::list::is_connected() {
local public_key="$1"
local ts
ts=$(cmd::list::last_handshake_ts "$public_key")
[[ -z "$ts" || "$ts" == "0" ]] && return 1
local now diff
now=$(date +%s)
diff=$(( now - ts ))
(( diff < 180 ))
}
function cmd::list::is_attempting() {
local name="$1"
local ts
ts=$(monitor::last_attempt "$name")
[[ -z "$ts" ]] && return 1
local now attempt_ts diff
now=$(date +%s)
attempt_ts=$(python3 -c "
from datetime import datetime, timezone
dt = datetime.fromisoformat('${ts}')
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
print(int(dt.timestamp()))
" 2>/dev/null || echo 0)
diff=$(( now - attempt_ts ))
(( diff < 180 ))
}
function cmd::list::is_blocked() {
local name="$1"
peers::is_blocked "$name"
}
function cmd::list::is_restricted() {
local name="$1"
[[ -f "$(ctx::block::path "${name}.block")" ]]
}
function cmd::list::format_last_seen() {
local name="$1"
local public_key="$2"
local ip="$3"
if cmd::list::is_blocked "$name"; then
local ts
ts=$(monitor::last_attempt "$name")
if [[ -n "$ts" ]]; then
# Format ISO timestamp
local formatted
formatted=$(python3 -c "
from datetime import datetime, timezone
dt = datetime.fromisoformat('${ts}')
print(dt.strftime('%Y-%m-%d %H:%M'))
" 2>/dev/null || echo "$ts")
echo "${formatted} (dropped)"
else
echo "—"
fi
else
local ts
ts=$(cmd::list::last_handshake_ts "$public_key")
if [[ -z "$ts" || "$ts" == "0" ]]; then
echo "—"
else
local formatted
formatted=$(date -d "@${ts}" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$ts")
echo "${formatted} (handshake)"
fi
fi
}
function cmd::list::format_status() {
local name="$1"
local public_key="$2"
local ip="$3" # new
local connected=false
local blocked=false
local restricted=false
cmd::list::is_blocked "$name" && blocked=true
cmd::list::is_restricted "$name" && restricted=true
if $blocked; then
cmd::list::is_attempting "$name" && connected=true
modifier=" (blocked)"
elif $restricted; then
cmd::list::is_connected "$public_key" && connected=true
modifier=" (restricted)"
else
cmd::list::is_connected "$public_key" && connected=true
modifier=""
fi
local conn_str
$connected && conn_str="online" || conn_str="offline"
local status="${conn_str}${modifier}"
local color
if $blocked; then
color="\033[1;31m"
elif $restricted; then
color="\033[1;33m"
elif $connected; then
color="\033[1;32m"
else
color="\033[0;37m"
fi
echo -e "${color}${status}\033[0m"
}
# ============================================
# Detail Card
# ============================================
function cmd::list::show_client() {
local name="$1"
local dir
dir="$(ctx::clients)"
local conf="${dir}/${name}.conf"
if [[ ! -f "$conf" ]]; then
log::error "Client not found: ${name}"
return 1
fi
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local allowed_ips
allowed_ips=$(grep "^AllowedIPs" "$conf" | awk '{print $3}')
local public_key
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
# Get endpoint
local endpoint="—"
if cmd::list::is_blocked "$name"; then
local ep
ep=$(monitor::last_endpoint "$name")
[[ -n "$ep" ]] && endpoint="$ep"
else
local ep
ep=$(monitor::endpoint_for_key "$public_key")
[[ -n "$ep" ]] && endpoint="$ep"
fi
# Determine type
local type="unknown"
for t in $(config::device_types); do
local subnet
subnet=$(config::subnet_for "$t")
if string::starts_with "$ip" "$subnet"; then
type="$t"
break
fi
done
local status
status=$(cmd::list::format_status "$name" "$public_key" "$ip")
local last_seen
last_seen=$(cmd::list::format_last_seen "$name" "$public_key" "$ip")
# Block rules
local block_file
block_file="$(ctx::block::path "${name}.block")"
local blocks=""
if [[ -f "$block_file" ]] && [[ -s "$block_file" ]]; then
while IFS=" " read -r client_ip target port proto; do
if [[ -z "$target" ]]; then
blocks+=" all traffic blocked\n"
else
local rule=" ${target}"
[[ -n "$port" ]] && rule+=":${port}/${proto}"
blocks+="${rule}\n"
fi
done < "$block_file"
fi
local sep
sep="$(printf '─%.0s' {1..50})"
echo ""
echo " ${sep}"
printf " \033[1;34m%-20s\033[0m %s\n" "Client:" "$name"
echo " ${sep}"
printf " %-20s %s\n" "IP:" "$ip"
printf " %-20s %s\n" "Type:" "$type"
printf " %-20s %b\n" "Status:" "$status"
printf " %-20s %s\n" "Endpoint:" "$endpoint"
printf " %-20s %s\n" "Last seen:" "$last_seen"
printf " %-20s %s\n" "Allowed IPs:" "$allowed_ips"
printf " %-20s %s\n" "Public key:" "$public_key"
if [[ -z "$blocks" ]]; then
printf " %-20s %s\n" "Blocks:" "none"
elif [[ "$blocks" == *"all traffic blocked"* ]]; then
printf " %-20s \033[1;31mAll\033[0m\n" "Blocks:"
else
printf " %-20s\n" "Blocks:"
echo -e "$blocks"
fi
echo " ${sep}"
echo ""
}
# ============================================
# Run
# ============================================
function cmd::list::run() {
local filter_type=""
local online_only=false
local offline_only=false
local restricted_only=false
local blocked_only=false
local allowed_only=false
local detailed=false
local single_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--type) filter_type="$2"; shift 2 ;;
--online) online_only=true; shift ;;
--offline) offline_only=true; shift ;;
--restricted) restricted_only=true; shift ;;
--blocked) blocked_only=true; shift ;;
--allowed) allowed_only=true; shift ;;
--detailed) detailed=true; shift ;;
--name) single_name="$2"; shift 2 ;;
--help) cmd::list::help; return ;;
*)
log::error "Unknown flag: $1"
cmd::list::help
return 1
;;
esac
done
# Single client detail card
if [[ -n "$single_name" ]]; then
cmd::list::show_client "$single_name"
return
fi
local dir
dir="$(ctx::clients)"
local confs=("${dir}"/*.conf)
if [[ ! -f "${confs[0]}" ]]; then
log::wg_list "No clients configured"
return 0
fi
# Detailed mode — cards only, no table
if $detailed; then
log::section "WireGuard Clients"
for conf in "${dir}"/*.conf; do
[[ -f "$conf" ]] || continue
local client_name
client_name=$(basename "$conf" .conf)
# Apply type filter
if [[ -n "$filter_type" ]]; then
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local type="unknown"
for t in $(config::device_types); do
local subnet
subnet=$(config::subnet_for "$t")
if string::starts_with "$ip" "$subnet"; then
type="$t"
break
fi
done
[[ "$type" != "$filter_type" ]] && continue
fi
cmd::list::show_client "$client_name"
done
return
fi
# Normal table view
log::section "WireGuard Clients"
printf "\n %-28s %-15s %-10s %-22s %s\n" \
"NAME" "IP" "TYPE" "STATUS" "LAST SEEN"
printf " %s\n" "$(printf '─%.0s' {1..90})"
for conf in "${dir}"/*.conf; do
[[ -f "$conf" ]] || continue
local client_name
client_name=$(basename "$conf" .conf)
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
# Determine type
local type="unknown"
for t in $(config::device_types); do
local subnet
subnet=$(config::subnet_for "$t")
if string::starts_with "$ip" "$subnet"; then
type="$t"
break
fi
done
# Apply type filter
if [[ -n "$filter_type" && "$type" != "$filter_type" ]]; then
continue
fi
local public_key
public_key=$(keys::public "$client_name" 2>/dev/null || echo "")
# Apply filters
if $online_only && ! cmd::list::is_connected "$public_key"; then
continue
fi
if $offline_only && cmd::list::is_connected "$public_key"; then
continue
fi
if $restricted_only && ! cmd::list::is_restricted "$client_name"; then
continue
fi
if $blocked_only && ! cmd::list::is_blocked "$client_name"; then
continue
fi
if $allowed_only && { cmd::list::is_blocked "$client_name" || cmd::list::is_restricted "$client_name"; }; then
continue
fi
local status
status=$(cmd::list::format_status "$client_name" "$public_key" "$ip")
local last_seen
last_seen=$(cmd::list::format_last_seen "$client_name" "$public_key" "$ip")
printf " %-28s %-15s %-10s %-32b %s\n" \
"$client_name" "$ip" "$type" "$status" "$last_seen"
done
printf "\n"
}