wgctl/commands/inspect.command.sh
Nuno Duque Nunes 87f6c770ef add README
2026-05-16 21:41:38 +00:00

287 lines
No EOL
7.6 KiB
Bash

#!/usr/bin/env bash
function cmd::inspect::on_load() {
flag::register --name
flag::register --type
flag::register --config
flag::register --qr
}
function cmd::inspect::help() {
cat <<EOF
Usage: wgctl inspect --name <name> [options]
wgctl inspect <full-name>
Show detailed information for a WireGuard client.
Sections shown:
Client — IP, type, rule, status, activity
Groups — group memberships
Rule — firewall rule with inheritance tree and service annotations
Peer Blocks — peer-specific restrictions (beyond the assigned rule)
Firewall — active iptables rules with ACCEPT/DROP counts
Options:
--name <name> Client name (e.g. phone-nuno)
--type <type> Device type — combines with --name
--config Also show raw WireGuard client config
--qr Also show QR code
Examples:
wgctl inspect --name phone-nuno
wgctl inspect --name nuno --type phone
wgctl inspect --name phone-nuno --config
wgctl inspect --name phone-nuno --qr
wgctl inspect guest-zephyr
EOF
}
# ============================================
# Private helpers
# ============================================
function cmd::inspect::_section() {
local title="$1"
printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title"
}
function cmd::inspect::_peer_info() {
local name="${1:-}"
local ip type rule public_key allowed_ips
ip=$(peers::get_ip "$name")
type=$(peers::get_type "$name")
rule=$(peers::get_meta "$name" "rule")
public_key=$(keys::public "$name" 2>/dev/null || echo "")
allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" \
2>/dev/null | cut -d'=' -f2- | xargs)
# Status
local handshake_ts is_blocked last_ts
handshake_ts=$(monitor::get_handshake_ts "$public_key")
peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
last_ts=$(monitor::last_attempt "$name")
local is_restricted="false"
block::has_specific_rules "$name" 2>/dev/null && is_restricted="true"
local status last_seen endpoint
status=$(peers::format_status "$name" "$public_key" \
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
last_seen=$(peers::format_last_seen "$name" "$public_key" \
"$is_blocked" "$last_ts" "" "$handshake_ts")
endpoint=$(monitor::get_cached_endpoint "$name")
local activity_total
activity_total=$(peers::format_activity_total "$public_key")
local activity_current
activity_current=$(peers::format_activity_current "$public_key")
local subtype
subtype=$(peers::get_meta "$name" "subtype")
local rule_extends=""
if [[ -n "$rule" ]]; then
local rule_file
rule_file="$(rule::path "$rule" 2>/dev/null)" || true
if [[ -n "$rule_file" ]]; then
local ext=()
mapfile -t ext < <(json::get "$rule_file" "extends" 2>/dev/null || true)
if [[ ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then
rule_extends=" (↳ ${ext[*]})"
fi
fi
fi
# Rule formatting
local rule_display="${rule:-}"
if [[ -n "$rule_file" && ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then
local extends_str
extends_str=$(printf '%s, ' "${ext[@]}" | sed 's/, $//')
rule_display="${rule} ↳ (${extends_str})"
fi
cmd::inspect::_section "Client"
ui::row "Name" "$name"
ui::row "IP" "$ip"
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
ui::row "Rule" "$rule_display"
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:-}"
ui::row "Activity (total)" "$activity_total"
ui::row "Activity (current)" "$activity_current"
return 0
}
function cmd::inspect::_rule_info() {
local name="${1:-}"
local rule
rule=$(peers::get_meta "$name" "rule")
[[ -z "$rule" ]] && return 0
rule::exists "$rule" || return 0
cmd::inspect::_section "Rule: ${rule}"
if rule::render_extends_tree "$rule"; then
printf "\n"
else
# No inheritance — flat view
rule::render_flat "$rule"
fi
return 0
}
function cmd::inspect::_blocks_info() {
local name="${1:-}"
block::has_file "$name" || return 0
cmd::inspect::_section "Peer Blocks"
local blocked_direct
blocked_direct=$(block::is_blocked_direct "$name")
[[ "$blocked_direct" == "true" ]] && \
printf " \033[1;31m🚫\033[0m blocked directly\n"
local blocked_groups
blocked_groups=$(block::get_groups "$name")
[[ -n "$blocked_groups" ]] && \
printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups"
block::format_rules "$name"
return 0
}
function cmd::inspect::_group_info() {
local name="$1"
local groups=()
mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name")
[[ ${#groups[@]} -eq 0 || -z "${groups[0]:-}" ]] && return 0
ui::section "Groups"
if [[ ${#groups[@]} -eq 0 ]] || [[ -z "${groups[0]:-}" ]]; then
printf " —\n"
return 0
fi
for g in "${groups[@]}"; do
[[ -z "$g" ]] && continue
local count
count=$(json::count "$(group::path "$g")" "peers")
printf " %-20s %s peers\n" "$g" "$count"
done
return 0
}
function cmd::inspect::_firewall_info() {
local name="${1:-}"
local ip
ip=$(peers::get_ip "$name")
local total=0 accepts=0 drops=0
local rules_output=()
while IFS= read -r line; do
[[ -z "$line" ]] && continue
(( total++ )) || true
[[ "$line" =~ ACCEPT ]] && (( accepts++ )) || true
[[ "$line" =~ DROP ]] && (( drops++ )) || true
rules_output+=("$line")
done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG)
[[ ${#rules_output[@]} -eq 0 || -z "${rules_output[0]:-}" ]] && return 0
# printf "\n \033[0;37m── Firewall (\033[0;32m+%d\033[0m \033[0;31m-%d\033[0m) \033[0m%s\n" \
# "$accepts" "$drops" "$(printf '─%.0s' {1..28})"
printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \
"$(color::green "+${accepts}")" \
"$(color::red "-${drops}")" \
"$(printf '─%.0s' {1..28})"
fw::list_peer_rules "$ip" false
# if [[ ${#rules_output[@]} -gt 0 ]]; then
# for line in "${rules_output[@]}"; do
# fw::format_rule "$line"
# done
# fi
return 0
}
function cmd::inspect::_config() {
local name="$1"
cmd::inspect::_section "Config"
printf "\n"
cat "$(ctx::clients)/${name}.conf"
printf "\n"
return 0
}
# ============================================
# Run
# ============================================
function cmd::inspect::run() {
local name="" type="" show_config=false show_qr=false
if [[ $# -gt 0 && "$1" != "--"* ]]; then
name="$1"
shift
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--type) type="$2"; shift 2 ;;
--config) show_config=true; shift ;;
--qr) show_qr=true; shift ;;
--help) cmd::inspect::help; return ;;
*)
log::error "Unknown flag: $1"
cmd::inspect::help
return 1
;;
esac
done
if [[ -z "$name" ]]; then
log::error "Missing required flag: --name"
cmd::inspect::help
return 1
fi
name=$(peers::resolve_and_require "$name" "$type") || return 1
load_command list
log::section "Inspect: ${name}"
cmd::inspect::_peer_info "$name"
cmd::inspect::_group_info "$name"
cmd::inspect::_rule_info "$name"
cmd::inspect::_blocks_info "$name"
cmd::inspect::_firewall_info "$name"
if $show_config; then
cmd::inspect::_config "$name"
fi
if $show_qr; then
cmd::inspect::_section "QR Code"
printf "\n"
load_command qr
cmd::qr::run --name "$name"
fi
printf "\n"
}