#!/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 < 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 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" }