#!/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 including status, rule with inheritance, groups, firewall rules, and activity. Options: --name Client name (e.g. phone-nuno) --type Device type — combines with --name (e.g. --name nuno --type phone) --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 status last_seen endpoint status=$(peers::format_status "$name" "$public_key" \ "$is_blocked" "false" "$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}" local rule_file rule_file="$(rule::path "$rule")" # Check for inheritance local extends_raw=() mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then # Show inheritance tree for base_name in "${extends_raw[@]}"; do [[ -z "$base_name" ]] && continue printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name" local base_allows base_blocks base_allows=$(rule::get "$base_name" "allow_ports")$'\n'$(rule::get "$base_name" "allow_ips") base_blocks=$(rule::get "$base_name" "block_ips")$'\n'$(rule::get "$base_name" "block_ports") while IFS= read -r e; do [[ -z "$e" ]] && continue printf " \033[0;32m+\033[0m %s\n" "$e" done <<< "$base_allows" while IFS= read -r e; do [[ -z "$e" ]] && continue printf " \033[0;31m-\033[0m %s\n" "$e" done <<< "$base_blocks" local base_dns base_dns=$(rule::get_own "$base_name" "dns_redirect") [[ "${base_dns,,}" == "true" ]] && \ printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" done # Own rules local own_allows own_blocks own_allows=$(json::get "$rule_file" "allow_ports" 2>/dev/null)$'\n'$(json::get "$rule_file" "allow_ips" 2>/dev/null) own_blocks=$(json::get "$rule_file" "block_ips" 2>/dev/null)$'\n'$(json::get "$rule_file" "block_ports" 2>/dev/null) local has_own=false while IFS= read -r e; do [[ -n "$e" ]] && has_own=true && break; done <<< "$own_allows$own_blocks" if $has_own; then printf "\n \033[0;37mOwn:\033[0m\n" while IFS= read -r e; do [[ -z "$e" ]] && continue printf " \033[0;32m+\033[0m %s\n" "$e" done <<< "$own_allows" while IFS= read -r e; do [[ -z "$e" ]] && continue printf " \033[0;31m-\033[0m %s\n" "$e" done <<< "$own_blocks" fi else # No inheritance — flat view local allow_ports allow_ips block_ips block_ports allow_ports=$(rule::get "$rule" "allow_ports") allow_ips=$(rule::get "$rule" "allow_ips") block_ips=$(rule::get "$rule" "block_ips") block_ports=$(rule::get "$rule" "block_ports") if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then printf "\n" while IFS= read -r e; do [[ -z "$e" ]] && continue printf " \033[0;32m+\033[0m %s\n" "$e" done <<< "$allow_ports"$'\n'"$allow_ips" fi if [[ -n "$block_ips" || -n "$block_ports" ]]; then printf "\n" while IFS= read -r e; do [[ -z "$e" ]] && continue printf " \033[0;31m-\033[0m %s\n" "$e" done <<< "$block_ips"$'\n'"$block_ports" fi if [[ -z "$allow_ports" && -z "$allow_ips" && \ -z "$block_ips" && -z "$block_ports" ]]; then printf "\n full access (no restrictions)\n" fi local dns_redirect dns_redirect=$(rule::get_own "$rule" "dns_redirect") [[ "${dns_redirect,,}" == "true" ]] && \ printf "\n \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" 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::_rule_info() { # local name="$1" # local rule # rule=$(peers::get_meta "$name" "rule") # [[ -z "$rule" ]] && return 0 # rule::exists "$rule" || return 0 # local rule_file # rule_file="$(ctx::rule::path "${rule}.rule")" # ui::section "Rule: ${rule}" # local desc dns_redirect # desc=$(json::get "$rule_file" "desc") # dns_redirect=$(json::get "$rule_file" "dns_redirect") # ui::row "Description" "${desc:-—}" # ui::row "DNS Redirect" "${dns_redirect:-false}" # local allow_ports allow_ips block_ips block_ports # allow_ports=$(json::get "$rule_file" "allow_ports") # allow_ips=$(json::get "$rule_file" "allow_ips") # block_ips=$(json::get "$rule_file" "block_ips") # block_ports=$(json::get "$rule_file" "block_ports") # if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then # printf " %-20s\n" "Allows:" # ui::print_list "+" "$allow_ports" # ui::print_list "+" "$allow_ips" # else # ui::row "Allows" "—" # fi # if [[ -n "$block_ips" || -n "$block_ports" ]]; then # printf " %-20s\n" "Blocks:" # ui::print_list "-" "$block_ips" # ui::print_list "-" "$block_ports" # else # ui::row "Blocks" "—" # fi # } function cmd::inspect::_group_info() { local name="$1" ui::section "Groups" local groups=() mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name") 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) # 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})" 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::_rule_info "$name" cmd::inspect::_group_info "$name" cmd::inspect::_firewall_info "$name" cmd::inspect::_blocks_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" }