#!/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 < [options] wgctl inspect 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 Client name (e.g. phone-nuno) --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" }