490 lines
No EOL
15 KiB
Bash
490 lines
No EOL
15 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# ============================================
|
|
# Lifecycle
|
|
# ============================================
|
|
|
|
function cmd::list::on_load() {
|
|
flag::register --type
|
|
flag::register --rule
|
|
flag::register --group
|
|
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 (desktop, laptop, phone, tablet, guest)
|
|
--rule <rule> Filter by assigned rule
|
|
--group <group> Filter by group membership
|
|
--online Show only connected clients
|
|
--offline Show only disconnected clients
|
|
--blocked Show only blocked clients
|
|
--restricted Show only restricted clients
|
|
--allowed Show only unrestricted clients
|
|
--detailed Show full detail cards for all clients
|
|
--name <name> Show detail card for a single client
|
|
|
|
Examples:
|
|
wgctl list
|
|
wgctl list --type guest
|
|
wgctl list --rule user
|
|
wgctl list --group family
|
|
wgctl list --online
|
|
wgctl list --blocked
|
|
wgctl list --detailed
|
|
wgctl list --name phone-nuno
|
|
EOF
|
|
}
|
|
|
|
# ============================================
|
|
# Header / Footer
|
|
# ============================================
|
|
|
|
function cmd::list::_render_header() {
|
|
local has_groups="$1"
|
|
if $has_groups; then
|
|
printf "\n %-28s %-15s %-13s %-12s %-12s %-22s %s\n" \
|
|
"NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
|
|
printf " %s\n" "$(printf '─%.0s' {1..135})"
|
|
else
|
|
printf "\n %-28s %-15s %-13s %-12s %-22s %s\n" \
|
|
"NAME" "IP" "TYPE" "RULE" "STATUS" "LAST SEEN"
|
|
printf " %s\n" "$(printf '─%.0s' {1..107})"
|
|
fi
|
|
}
|
|
|
|
function cmd::list::_render_footer() {
|
|
local has_groups="$1"
|
|
if $has_groups; then
|
|
printf " %s\n" "$(printf '─%.0s' {1..135})"
|
|
else
|
|
printf " %s\n" "$(printf '─%.0s' {1..107})"
|
|
fi
|
|
}
|
|
|
|
function cmd::list::_render_summary() {
|
|
local group_summary="${1:-}"
|
|
local -n _rule_counts="$2"
|
|
|
|
# Count total from rule_counts (only filtered peers)
|
|
local total=0
|
|
for r in "${!_rule_counts[@]}"; do
|
|
(( total += _rule_counts[$r] )) || true
|
|
done
|
|
|
|
local summary=""
|
|
for r in "${!_rule_counts[@]}"; do
|
|
summary+="${_rule_counts[$r]} ${r}, "
|
|
done
|
|
summary="${summary%, }"
|
|
|
|
if [[ -n "$group_summary" ]]; then
|
|
printf "\n Showing %s peers [%s] — %s\n\n" "$total" "$summary" "$group_summary"
|
|
else
|
|
printf "\n Showing %s peers [%s]\n\n" "$total" "$summary"
|
|
fi
|
|
}
|
|
|
|
# ============================================
|
|
# Detail Card (show_client)
|
|
# ============================================
|
|
|
|
function cmd::list::show_client() {
|
|
local name="${1:-}"
|
|
local conf
|
|
conf="$(ctx::clients)/${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" | cut -d'=' -f2- | xargs)
|
|
|
|
local public_key
|
|
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
|
|
|
local type
|
|
type=$(peers::get_type_from_ip "$ip")
|
|
|
|
local subtype
|
|
subtype=$(peers::get_meta "$name" "subtype")
|
|
|
|
local endpoint="—"
|
|
local is_blocked="false"
|
|
peers::is_blocked "$name" && is_blocked="true"
|
|
|
|
if [[ "$is_blocked" == "true" ]]; 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
|
|
|
|
local handshake_ts
|
|
handshake_ts=$(monitor::get_handshake_ts "$public_key")
|
|
|
|
local last_ts
|
|
last_ts=$(monitor::last_attempt "$name")
|
|
|
|
local status
|
|
status=$(peers::format_status "$name" "$public_key" \
|
|
"$is_blocked" "false" "$handshake_ts" "$last_ts")
|
|
|
|
local last_seen
|
|
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
|
|
|
local blocks=""
|
|
if block::has_file "$name" && block::is_blocked "$name"; then
|
|
if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then
|
|
blocks="all traffic blocked"
|
|
fi
|
|
local rule_lines
|
|
rule_lines=$(block::format_rules "$name")
|
|
[[ -n "$rule_lines" ]] && blocks+="$rule_lines"
|
|
fi
|
|
|
|
ui::section "Client: ${name}"
|
|
ui::row "IP" "$ip"
|
|
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
|
|
ui::row "Status" "$(echo -e "$status")"
|
|
ui::row "Endpoint" "$endpoint"
|
|
ui::row "Last seen" "$last_seen"
|
|
ui::row "AllowedIPs" "$allowed_ips"
|
|
ui::row "Public key" "$public_key"
|
|
|
|
if [[ -z "$blocks" ]]; then
|
|
ui::row "Blocks" "none"
|
|
elif [[ "$blocks" == *"all traffic blocked"* ]]; then
|
|
ui::row "Blocks" "$(echo -e "\033[1;31mAll\033[0m")"
|
|
else
|
|
printf " %-20s\n" "Blocks:"
|
|
echo -e "$blocks"
|
|
fi
|
|
printf "\n"
|
|
}
|
|
|
|
# ============================================
|
|
# Run
|
|
# ============================================
|
|
|
|
function cmd::list::run() {
|
|
local filter_type="" filter_rule="" filter_group=""
|
|
local online_only=false offline_only=false
|
|
local restricted_only=false blocked_only=false allowed_only=false
|
|
local detailed=false single_name=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--type) filter_type="$2"; shift 2 ;;
|
|
--rule) filter_rule="$2"; shift 2 ;;
|
|
--group) filter_group="$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 detail card
|
|
if [[ -n "$single_name" ]]; then
|
|
cmd::list::show_client "$single_name"
|
|
return 0
|
|
fi
|
|
|
|
local dir
|
|
dir="$(ctx::clients)"
|
|
local confs=("${dir}"/*.conf)
|
|
if [[ ! -f "${confs[0]}" ]]; then
|
|
log::wg_warning "No clients configured"
|
|
return 0
|
|
fi
|
|
|
|
# ── Precompute everything ──────────────────
|
|
cmd::list::_precompute_all
|
|
|
|
# ── Detailed mode ──────────────────────────
|
|
if $detailed; then
|
|
log::section "WireGuard Clients"
|
|
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
|
|
return 0
|
|
fi
|
|
|
|
# ── Build filter description ───────────────
|
|
local filter_desc=""
|
|
cmd::list::_build_filter_desc
|
|
|
|
# ── Table view ─────────────────────────────
|
|
declare -A rule_counts=() group_counts=()
|
|
_list_header_printed=false
|
|
|
|
cmd::list::_iter_confs "$filter_type" cmd::list::_render_row
|
|
|
|
if [[ "$_list_header_printed" == "true" ]]; then
|
|
cmd::list::_render_footer $has_groups
|
|
local group_summary=""
|
|
cmd::list::_build_group_summary
|
|
cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc"
|
|
else
|
|
log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}"
|
|
fi
|
|
}
|
|
|
|
function cmd::list::_iter_confs() {
|
|
# Usage: cmd::list::_iter_confs <filter_type> <callback>
|
|
local filter_type="$1"
|
|
local callback="$2"
|
|
local dir
|
|
dir="$(ctx::clients)"
|
|
|
|
for conf in "${dir}"/*.conf; do
|
|
[[ -f "$conf" ]] || continue
|
|
local client_name
|
|
client_name=$(basename "$conf" .conf)
|
|
local ip="${p_ips[$client_name]:-}"
|
|
if [[ -z "$ip" ]]; then
|
|
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
|
fi
|
|
local type
|
|
type=$(peers::get_type_from_ip "$ip")
|
|
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
|
"$callback" "$client_name" "$ip" "$type"
|
|
done
|
|
}
|
|
|
|
function cmd::list::_render_row() {
|
|
local client_name="$1" ip="$2" type="$3"
|
|
|
|
local pubkey="${p_pubkeys[$client_name]:-}"
|
|
local handshake_ts="${wg_handshakes[$pubkey]:-0}"
|
|
local is_blocked="${p_blocked[$client_name]:-false}"
|
|
local is_restricted="${p_restricted[$client_name]:-false}"
|
|
local last_ts="${p_last_ts[$client_name]:-}"
|
|
|
|
# Apply status filters
|
|
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
|
if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
|
if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
|
|
if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
|
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
|
[[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
|
|
|
if [[ -n "$filter_group" ]]; then
|
|
local peer_group="${peer_group_map[$client_name]:-}"
|
|
[[ "$peer_group" != "$filter_group" ]] && return 0
|
|
fi
|
|
|
|
# Format display values
|
|
local status last_seen display_type rule group_display
|
|
status=$(peers::format_status "$client_name" "$pubkey" \
|
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
|
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
|
display_type=$(peers::display_type "$type" "${p_subtypes[$client_name]:-}")
|
|
rule="${p_rules[$client_name]:-—}"
|
|
|
|
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
|
|
|
# Print header on first match
|
|
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
|
log::section "WireGuard Clients"
|
|
cmd::list::_render_header $has_groups
|
|
_list_header_printed=true
|
|
fi
|
|
|
|
# Update rule counts for summary (outer scope array)
|
|
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
|
|
|
# Pad status
|
|
local padded_status
|
|
padded_status=$(ui::pad_status "$status" 25)
|
|
|
|
# Render row
|
|
if $has_groups; then
|
|
group_display="${peer_group_map[$client_name]:-—}"
|
|
|
|
if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
|
|
group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
|
|
fi
|
|
|
|
local rule_col_width=12 group_col_width=12
|
|
[[ "$rule" == "—" ]] && rule_col_width=14
|
|
[[ "$group_display" == "—" ]] && group_col_width=14
|
|
printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
|
|
"$client_name" "$ip" "$display_type" "$rule" \
|
|
"$group_display" "$padded_status" "$last_seen"
|
|
else
|
|
local rule_col_width=12
|
|
[[ "$rule" == "—" ]] && rule_col_width=14
|
|
printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \
|
|
"$client_name" "$ip" "$display_type" "$rule" \
|
|
"$padded_status" "$last_seen"
|
|
fi
|
|
}
|
|
|
|
# ============================================
|
|
# Private helpers
|
|
# ============================================
|
|
|
|
function cmd::list::_precompute_all() {
|
|
# Peer data
|
|
declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=()
|
|
while IFS="|" read -r name ip rule subtype last_ts last_evt; do
|
|
[[ -z "$name" ]] && continue
|
|
p_ips["$name"]="$ip"
|
|
p_rules["$name"]="${rule:-—}"
|
|
p_subtypes["$name"]="$subtype"
|
|
p_last_ts["$name"]="$last_ts"
|
|
p_last_evt["$name"]="$last_evt"
|
|
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
|
|
|
# WireGuard handshakes + endpoints
|
|
declare -gA wg_handshakes=() wg_endpoints=()
|
|
while IFS=$'\t' read -r pubkey ts; do
|
|
[[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
|
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
|
while IFS=$'\t' read -r pubkey endpoint; do
|
|
[[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
|
done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
|
|
|
# Block/restricted status
|
|
declare -gA p_blocked=() p_restricted=()
|
|
local wg_peers
|
|
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
|
while IFS= read -r name; do
|
|
if block::is_blocked "$name" 2>/dev/null; then
|
|
p_restricted["$name"]=true
|
|
else
|
|
p_restricted["$name"]=false
|
|
fi
|
|
local pubkey
|
|
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
|
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
|
p_blocked["$name"]=true
|
|
else
|
|
p_blocked["$name"]=false
|
|
fi
|
|
done < <(peers::all)
|
|
|
|
# Public keys
|
|
declare -gA p_pubkeys=()
|
|
local dir
|
|
dir="$(ctx::clients)"
|
|
for kf in "${dir}"/*_public.key; do
|
|
[[ -f "$kf" ]] || continue
|
|
local kname
|
|
kname=$(basename "$kf" _public.key)
|
|
p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
|
done
|
|
|
|
# Groups
|
|
has_groups=false
|
|
declare -gA peer_group_map=()
|
|
local groups_dir
|
|
groups_dir="$(ctx::groups)"
|
|
local group_files=("${groups_dir}"/*.group)
|
|
if [[ -f "${group_files[0]}" ]]; then
|
|
has_groups=true
|
|
while IFS=":" read -r peer_name group_name; do
|
|
[[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
|
|
done < <(json::peer_group_map "$groups_dir")
|
|
fi
|
|
|
|
# Transfer/activity data — keyed by pubkey
|
|
declare -gA p_rx=() p_tx=() p_activity=()
|
|
while IFS="|" read -r pubkey rx tx level; do
|
|
[[ -z "$pubkey" ]] && continue
|
|
p_rx["$pubkey"]="$rx"
|
|
p_tx["$pubkey"]="$tx"
|
|
p_activity["$pubkey"]="$level"
|
|
done < <(json::peer_transfer "$(config::interface)")
|
|
}
|
|
|
|
function cmd::list::_precompute_block_status() {
|
|
local -n _blocked="$1"
|
|
local -n _restricted="$2"
|
|
|
|
local wg_peers
|
|
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
|
|
|
while IFS= read -r name; do
|
|
# Restricted = has block rules but still in server (partial block)
|
|
if block::is_blocked "$name" 2>/dev/null; then
|
|
_restricted["$name"]=true
|
|
else
|
|
_restricted["$name"]=false
|
|
fi
|
|
|
|
# Blocked = removed from WG server
|
|
local pubkey
|
|
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
|
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
|
_blocked["$name"]=true
|
|
else
|
|
_blocked["$name"]=false
|
|
fi
|
|
done < <(peers::all)
|
|
}
|
|
|
|
function cmd::list::_build_filter_desc() {
|
|
filter_desc=""
|
|
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
|
[[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
|
|
[[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
|
|
$online_only && filter_desc+="online "
|
|
$offline_only && filter_desc+="offline "
|
|
$blocked_only && filter_desc+="blocked "
|
|
filter_desc="${filter_desc% }"
|
|
}
|
|
|
|
function cmd::list::_build_group_summary() {
|
|
group_summary=""
|
|
if $has_groups; then
|
|
declare -A _gs=()
|
|
for peer in "${!peer_group_map[@]}"; do
|
|
local g="${peer_group_map[$peer]}"
|
|
_gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true
|
|
done
|
|
for g in "${!_gs[@]}"; do
|
|
group_summary+="${_gs[$g]} in ${g}, "
|
|
done
|
|
group_summary="${group_summary%, }"
|
|
fi
|
|
}
|
|
|
|
function cmd::list::_show_client_safe() {
|
|
local name="$1"
|
|
cmd::list::show_client "$name" || true
|
|
} |