inspect.command.sh
· 12 KiB · Bash
Surowy
#!/usr/bin/env bash
function cmd::inspect::on_load() {
flag::register --name
flag::register --type
flag::register --config
flag::register --qr
command::mixin json_output
}
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
}
INSPECT_WIDTH=48 # total visible width of section lines
INSPECT_LABEL_WIDTH=20
# ============================================
# Private helpers
# ============================================
function cmd::inspect::_section() {
local title="${1:-}" extra="${2:-0}"
local width=$(( INSPECT_WIDTH + extra ))
local title_len=${#title}
# Account for "── " (3) + " " (1) before dashes
local dash_count=$(( width - title_len - 4 ))
[[ $dash_count -lt 2 ]] && dash_count=2
local dashes
dashes=$(printf '─%.0s' $(seq 1 $dash_count))
printf "\n \033[0;37m── %s %s\033[0m\n" "$title" "$dashes"
}
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_verbose "$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 rule_file=""
local rule_extends=""
if [[ -n "$rule" ]]; then
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"
printf "\n"
ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
ui::row "Type" "$(peers::display_type "$type")" "${INSPECT_LABEL_WIDTH}"
ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}"
ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}"
ui::row "AllowedIPs" "$allowed_ips" "${INSPECT_LABEL_WIDTH}"
ui::row "Public key" "${public_key:-—}" "${INSPECT_LABEL_WIDTH}"
ui::row "Activity (total)" "$activity_total" "${INSPECT_LABEL_WIDTH}"
ui::row "Activity (current)" "$activity_current" "${INSPECT_LABEL_WIDTH}"
return 0
}
function cmd::inspect::_rule_separator() {
local line_width=20
local total=$INSPECT_WIDTH
local pad=$(( (total - line_width) / 2 ))
printf "\n%*s\033[2m%s\033[0m\n\n" "$pad" "" "$(printf '─%.0s' $(seq 1 $line_width))"
}
function cmd::inspect::_rule_info() {
local name="${1:-}"
local rule
rule=$(peers::get_meta "$name" "rule")
local identity_name identity_rules strict
identity_name=$(identity::get_name "$name")
if [[ -n "$identity_name" ]]; then
identity_rules=$(identity::rules "$identity_name")
strict=$(identity::rule_flags "$identity_name" "strict_rule")
fi
# Skip section entirely if nothing to show
[[ -z "$rule" && -z "$identity_rules" ]] && return 0
# Build section header
local header="Rules"
[[ -n "$rule" ]] && header="${header}: ${rule}"
[[ -n "$identity_name" && -n "$identity_rules" ]] && \
header="${header} · identity:${identity_name}"
cmd::inspect::_section "$header"
# Identity block first
if [[ -n "$identity_name" && -n "$identity_rules" ]]; then
ui::rule::identity_block "$identity_name" "$strict"
fi
# Peer rule block — only if set and not suppressed
if [[ -n "$rule" ]]; then
rule::exists "$rule" || return 0
if [[ -n "$identity_rules" ]]; then
# Both identity and peer rules exist — show peer block with same pattern
printf "\n \033[0;37m· peer:%s\033[0m\n" "$name"
ui::rule::_peer_rule_entry "$rule"
else
# Only peer rule — render directly without peer: label
printf "\n"
if rule::render_extends_tree "$rule"; then
:
else
rule::render_flat "$rule"
fi
fi
elif [[ "$strict" == "true" && -n "$rule" ]]; then
printf "\n \033[2mpeer rule '%s' suppressed by strict policy\033[0m\n" "$rule"
fi
return 0
}
function cmd::inspect::_blocks_info() {
local name="${1:-}"
block::has_file "$name" || return 0
local blocked_direct
blocked_direct=$(block::is_blocked_direct "$name")
local blocked_groups
blocked_groups=$(block::get_groups "$name")
local rules_output
rules_output=$(block::get_rules "$name")
# Skip if truly empty
if [[ "$blocked_direct" != "true" ]] && \
ui::empty "$blocked_groups" && \
ui::empty "$rules_output"; then
block::cleanup "$name" # clean up stale empty file
return 0
fi
# Count rules for header
local rule_count=0
while IFS= read -r line; do
[[ -n "$line" ]] && (( rule_count++ )) || true
done <<< "$rules_output"
# Build header like firewall: Blocks (+N)
local header_counts=""
[[ "$rule_count" -gt 0 ]] && header_counts=" (${rule_count})"
[[ "$blocked_direct" == "true" || -n "$blocked_groups" ]] && \
header_counts="${header_counts} 🚫"
cmd::inspect::_section "Blocks${header_counts}"
printf "\n"
[[ "$blocked_direct" == "true" ]] && \
printf " \033[1;31m🚫\033[0m blocked directly\n"
[[ -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")
ui::empty "${groups[*]}" && return 0
local count=${#groups[@]}
cmd::inspect::_section "Groups (${count})"
printf "\n"
for g in "${groups[@]}"; do
[[ -z "$g" ]] && continue
local peer_count
local main_marker=""
peer_count=$(json::count "$(group::path "$g")" "peers")
[[ "$g" == "$(peers::get_main_group "$name")" ]] && \
main_marker=" \033[0;33m★\033[0m"
printf " \033[0;37m·\033[0m %-20s \033[0;37m%s peers\033[0m%b\n" \
"$g" "$peer_count" "$main_marker"
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)
ui::empty "${rules_output[*]}" && return 0
printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \
"$(color::green "+${accepts}")" \
"$(color::red "-${drops}")" \
"$(printf '\033[0;37m─%.0s' {1..28})"
fw::list_peer_rules "$ip" false
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
if command::json; then
cmd::inspect::_output_json "$name"
return 0
fi
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"
}
# ============================================
# JSON (API consumption)
# ============================================
function cmd::inspect::_output_json() {
local name="${1:-}"
local ip type rule allowed_ips public_key is_blocked status
ip=$(peers::get_ip "$name")
type=$(peers::get_type "$name")
rule=$(peers::get_meta "$name" "rule")
allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" 2>/dev/null | \
awk '{print $3}' | tr -d ',')
public_key=$(keys::public "$name" 2>/dev/null || echo "")
peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
# Handshake status
local handshake_ts=0
handshake_ts=$(wg show "$(config::interface)" latest-handshakes 2>/dev/null | \
grep "$public_key" | awk '{print $2}') || handshake_ts=0
local last_ts
last_ts=$(peers::get_meta "$name" "last_ts" 2>/dev/null || echo "")
local conn_state
conn_state=$(peers::connection_state "$is_blocked" "false" \
"${handshake_ts:-0}" "${last_ts:-}" | cut -d'|' -f1)
# Groups
local groups_json="[]"
local -a group_list=()
while IFS= read -r g; do
[[ -n "$g" ]] && group_list+=("\"$g\"")
done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null)
[[ ${#group_list[@]} -gt 0 ]] && \
groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
# Identity
local identity
identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
# Rule extends
local rule_extends="[]"
if [[ -n "$rule" ]]; then
local rule_file
rule_file=$(json::find_rule_file "$(ctx::rules)" "$rule" 2>/dev/null)
if [[ -n "$rule_file" ]]; then
local -a extends=()
while IFS= read -r ext; do
[[ -n "$ext" ]] && extends+=("\"$ext\"")
done < <(json::get "$rule_file" "extends" 2>/dev/null)
[[ ${#extends[@]} -gt 0 ]] && \
rule_extends="[$(printf '%s,' "${extends[@]}" | sed 's/,$//')]"
fi
fi
local data
data=$(printf '{"name":"%s","ip":"%s","type":"%s","rule":"%s","rule_extends":%s,"allowed_ips":"%s","public_key":"%s","is_blocked":%s,"status":"%s","identity":"%s","groups":%s}' \
"$name" "$ip" "$type" \
"${rule:-}" "$rule_extends" \
"${allowed_ips:-}" "$public_key" \
"$is_blocked" "$conn_state" \
"${identity:-}" "$groups_json")
printf '%s' "$data" | json::envelope "inspect" "1"
}
| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | function cmd::inspect::on_load() { |
| 4 | flag::register --name |
| 5 | flag::register --type |
| 6 | flag::register --config |
| 7 | flag::register --qr |
| 8 | |
| 9 | command::mixin json_output |
| 10 | } |
| 11 | |
| 12 | function cmd::inspect::help() { |
| 13 | cat <<EOF |
| 14 | Usage: wgctl inspect --name <name> [options] |
| 15 | wgctl inspect <full-name> |
| 16 | |
| 17 | Show detailed information for a WireGuard client. |
| 18 | |
| 19 | Sections shown: |
| 20 | Client — IP, type, rule, status, activity |
| 21 | Groups — group memberships |
| 22 | Rule — firewall rule with inheritance tree and service annotations |
| 23 | Peer Blocks — peer-specific restrictions (beyond the assigned rule) |
| 24 | Firewall — active iptables rules with ACCEPT/DROP counts |
| 25 | |
| 26 | Options: |
| 27 | --name <name> Client name (e.g. phone-nuno) |
| 28 | --type <type> Device type — combines with --name |
| 29 | --config Also show raw WireGuard client config |
| 30 | --qr Also show QR code |
| 31 | |
| 32 | Examples: |
| 33 | wgctl inspect --name phone-nuno |
| 34 | wgctl inspect --name nuno --type phone |
| 35 | wgctl inspect --name phone-nuno --config |
| 36 | wgctl inspect --name phone-nuno --qr |
| 37 | wgctl inspect guest-zephyr |
| 38 | EOF |
| 39 | } |
| 40 | |
| 41 | INSPECT_WIDTH=48 # total visible width of section lines |
| 42 | INSPECT_LABEL_WIDTH=20 |
| 43 | |
| 44 | # ============================================ |
| 45 | # Private helpers |
| 46 | # ============================================ |
| 47 | |
| 48 | |
| 49 | function cmd::inspect::_section() { |
| 50 | local title="${1:-}" extra="${2:-0}" |
| 51 | local width=$(( INSPECT_WIDTH + extra )) |
| 52 | local title_len=${#title} |
| 53 | # Account for "── " (3) + " " (1) before dashes |
| 54 | local dash_count=$(( width - title_len - 4 )) |
| 55 | [[ $dash_count -lt 2 ]] && dash_count=2 |
| 56 | local dashes |
| 57 | dashes=$(printf '─%.0s' $(seq 1 $dash_count)) |
| 58 | printf "\n \033[0;37m── %s %s\033[0m\n" "$title" "$dashes" |
| 59 | } |
| 60 | |
| 61 | function cmd::inspect::_peer_info() { |
| 62 | local name="${1:-}" |
| 63 | |
| 64 | local ip type rule public_key allowed_ips |
| 65 | ip=$(peers::get_ip "$name") |
| 66 | type=$(peers::get_type "$name") |
| 67 | rule=$(peers::get_meta "$name" "rule") |
| 68 | public_key=$(keys::public "$name" 2>/dev/null || echo "") |
| 69 | allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" \ |
| 70 | 2>/dev/null | cut -d'=' -f2- | xargs) |
| 71 | |
| 72 | # Status |
| 73 | local handshake_ts is_blocked last_ts |
| 74 | handshake_ts=$(monitor::get_handshake_ts "$public_key") |
| 75 | peers::is_blocked "$name" && is_blocked="true" || is_blocked="false" |
| 76 | last_ts=$(monitor::last_attempt "$name") |
| 77 | |
| 78 | local is_restricted="false" |
| 79 | block::has_specific_rules "$name" 2>/dev/null && is_restricted="true" |
| 80 | |
| 81 | local status last_seen endpoint |
| 82 | status=$(peers::format_status_verbose "$name" "$public_key" \ |
| 83 | "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") |
| 84 | last_seen=$(peers::format_last_seen "$name" "$public_key" \ |
| 85 | "$is_blocked" "$last_ts" "" "$handshake_ts") |
| 86 | endpoint=$(monitor::get_cached_endpoint "$name") |
| 87 | |
| 88 | local activity_total |
| 89 | activity_total=$(peers::format_activity_total "$public_key") |
| 90 | |
| 91 | local activity_current |
| 92 | activity_current=$(peers::format_activity_current "$public_key") |
| 93 | |
| 94 | local rule_file="" |
| 95 | local rule_extends="" |
| 96 | if [[ -n "$rule" ]]; then |
| 97 | rule_file="$(rule::path "$rule" 2>/dev/null)" || true |
| 98 | if [[ -n "$rule_file" ]]; then |
| 99 | local ext=() |
| 100 | mapfile -t ext < <(json::get "$rule_file" "extends" 2>/dev/null || true) |
| 101 | if [[ ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then |
| 102 | rule_extends=" (↳ ${ext[*]})" |
| 103 | fi |
| 104 | fi |
| 105 | fi |
| 106 | |
| 107 | # Rule formatting |
| 108 | local rule_display="${rule:-—}" |
| 109 | if [[ -n "$rule_file" && ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then |
| 110 | local extends_str |
| 111 | extends_str=$(printf '%s, ' "${ext[@]}" | sed 's/, $//') |
| 112 | rule_display="${rule} ↳ (${extends_str})" |
| 113 | fi |
| 114 | |
| 115 | cmd::inspect::_section "Client" |
| 116 | printf "\n" |
| 117 | ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}" |
| 118 | ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}" |
| 119 | ui::row "Type" "$(peers::display_type "$type")" "${INSPECT_LABEL_WIDTH}" |
| 120 | ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}" |
| 121 | ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}" |
| 122 | ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}" |
| 123 | ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}" |
| 124 | ui::row "AllowedIPs" "$allowed_ips" "${INSPECT_LABEL_WIDTH}" |
| 125 | ui::row "Public key" "${public_key:-—}" "${INSPECT_LABEL_WIDTH}" |
| 126 | ui::row "Activity (total)" "$activity_total" "${INSPECT_LABEL_WIDTH}" |
| 127 | ui::row "Activity (current)" "$activity_current" "${INSPECT_LABEL_WIDTH}" |
| 128 | |
| 129 | return 0 |
| 130 | } |
| 131 | |
| 132 | function cmd::inspect::_rule_separator() { |
| 133 | local line_width=20 |
| 134 | local total=$INSPECT_WIDTH |
| 135 | local pad=$(( (total - line_width) / 2 )) |
| 136 | printf "\n%*s\033[2m%s\033[0m\n\n" "$pad" "" "$(printf '─%.0s' $(seq 1 $line_width))" |
| 137 | } |
| 138 | |
| 139 | function cmd::inspect::_rule_info() { |
| 140 | local name="${1:-}" |
| 141 | local rule |
| 142 | rule=$(peers::get_meta "$name" "rule") |
| 143 | |
| 144 | local identity_name identity_rules strict |
| 145 | identity_name=$(identity::get_name "$name") |
| 146 | if [[ -n "$identity_name" ]]; then |
| 147 | identity_rules=$(identity::rules "$identity_name") |
| 148 | strict=$(identity::rule_flags "$identity_name" "strict_rule") |
| 149 | fi |
| 150 | |
| 151 | # Skip section entirely if nothing to show |
| 152 | [[ -z "$rule" && -z "$identity_rules" ]] && return 0 |
| 153 | |
| 154 | # Build section header |
| 155 | local header="Rules" |
| 156 | [[ -n "$rule" ]] && header="${header}: ${rule}" |
| 157 | [[ -n "$identity_name" && -n "$identity_rules" ]] && \ |
| 158 | header="${header} · identity:${identity_name}" |
| 159 | |
| 160 | cmd::inspect::_section "$header" |
| 161 | |
| 162 | # Identity block first |
| 163 | if [[ -n "$identity_name" && -n "$identity_rules" ]]; then |
| 164 | ui::rule::identity_block "$identity_name" "$strict" |
| 165 | fi |
| 166 | |
| 167 | # Peer rule block — only if set and not suppressed |
| 168 | if [[ -n "$rule" ]]; then |
| 169 | rule::exists "$rule" || return 0 |
| 170 | |
| 171 | if [[ -n "$identity_rules" ]]; then |
| 172 | # Both identity and peer rules exist — show peer block with same pattern |
| 173 | printf "\n \033[0;37m· peer:%s\033[0m\n" "$name" |
| 174 | ui::rule::_peer_rule_entry "$rule" |
| 175 | else |
| 176 | # Only peer rule — render directly without peer: label |
| 177 | printf "\n" |
| 178 | if rule::render_extends_tree "$rule"; then |
| 179 | : |
| 180 | else |
| 181 | rule::render_flat "$rule" |
| 182 | fi |
| 183 | fi |
| 184 | elif [[ "$strict" == "true" && -n "$rule" ]]; then |
| 185 | printf "\n \033[2mpeer rule '%s' suppressed by strict policy\033[0m\n" "$rule" |
| 186 | fi |
| 187 | |
| 188 | return 0 |
| 189 | } |
| 190 | |
| 191 | function cmd::inspect::_blocks_info() { |
| 192 | local name="${1:-}" |
| 193 | block::has_file "$name" || return 0 |
| 194 | |
| 195 | local blocked_direct |
| 196 | blocked_direct=$(block::is_blocked_direct "$name") |
| 197 | |
| 198 | local blocked_groups |
| 199 | blocked_groups=$(block::get_groups "$name") |
| 200 | |
| 201 | local rules_output |
| 202 | rules_output=$(block::get_rules "$name") |
| 203 | |
| 204 | # Skip if truly empty |
| 205 | if [[ "$blocked_direct" != "true" ]] && \ |
| 206 | ui::empty "$blocked_groups" && \ |
| 207 | ui::empty "$rules_output"; then |
| 208 | block::cleanup "$name" # clean up stale empty file |
| 209 | return 0 |
| 210 | fi |
| 211 | |
| 212 | # Count rules for header |
| 213 | local rule_count=0 |
| 214 | while IFS= read -r line; do |
| 215 | [[ -n "$line" ]] && (( rule_count++ )) || true |
| 216 | done <<< "$rules_output" |
| 217 | |
| 218 | # Build header like firewall: Blocks (+N) |
| 219 | local header_counts="" |
| 220 | [[ "$rule_count" -gt 0 ]] && header_counts=" (${rule_count})" |
| 221 | [[ "$blocked_direct" == "true" || -n "$blocked_groups" ]] && \ |
| 222 | header_counts="${header_counts} 🚫" |
| 223 | |
| 224 | cmd::inspect::_section "Blocks${header_counts}" |
| 225 | printf "\n" |
| 226 | |
| 227 | [[ "$blocked_direct" == "true" ]] && \ |
| 228 | printf " \033[1;31m🚫\033[0m blocked directly\n" |
| 229 | [[ -n "$blocked_groups" ]] && \ |
| 230 | printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups" |
| 231 | |
| 232 | block::format_rules "$name" |
| 233 | return 0 |
| 234 | } |
| 235 | |
| 236 | function cmd::inspect::_group_info() { |
| 237 | local name="$1" |
| 238 | |
| 239 | local groups=() |
| 240 | mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name") |
| 241 | |
| 242 | ui::empty "${groups[*]}" && return 0 |
| 243 | |
| 244 | local count=${#groups[@]} |
| 245 | cmd::inspect::_section "Groups (${count})" |
| 246 | printf "\n" |
| 247 | |
| 248 | for g in "${groups[@]}"; do |
| 249 | [[ -z "$g" ]] && continue |
| 250 | local peer_count |
| 251 | local main_marker="" |
| 252 | peer_count=$(json::count "$(group::path "$g")" "peers") |
| 253 | [[ "$g" == "$(peers::get_main_group "$name")" ]] && \ |
| 254 | main_marker=" \033[0;33m★\033[0m" |
| 255 | printf " \033[0;37m·\033[0m %-20s \033[0;37m%s peers\033[0m%b\n" \ |
| 256 | "$g" "$peer_count" "$main_marker" |
| 257 | done |
| 258 | |
| 259 | return 0 |
| 260 | } |
| 261 | |
| 262 | function cmd::inspect::_firewall_info() { |
| 263 | local name="${1:-}" |
| 264 | local ip |
| 265 | ip=$(peers::get_ip "$name") |
| 266 | |
| 267 | local total=0 accepts=0 drops=0 |
| 268 | local rules_output=() |
| 269 | while IFS= read -r line; do |
| 270 | [[ -z "$line" ]] && continue |
| 271 | (( total++ )) || true |
| 272 | [[ "$line" =~ ACCEPT ]] && (( accepts++ )) || true |
| 273 | [[ "$line" =~ DROP ]] && (( drops++ )) || true |
| 274 | rules_output+=("$line") |
| 275 | done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG) |
| 276 | |
| 277 | ui::empty "${rules_output[*]}" && return 0 |
| 278 | |
| 279 | printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \ |
| 280 | "$(color::green "+${accepts}")" \ |
| 281 | "$(color::red "-${drops}")" \ |
| 282 | "$(printf '\033[0;37m─%.0s' {1..28})" |
| 283 | |
| 284 | fw::list_peer_rules "$ip" false |
| 285 | |
| 286 | return 0 |
| 287 | } |
| 288 | |
| 289 | function cmd::inspect::_config() { |
| 290 | local name="$1" |
| 291 | cmd::inspect::_section "Config" |
| 292 | printf "\n" |
| 293 | cat "$(ctx::clients)/${name}.conf" |
| 294 | printf "\n" |
| 295 | |
| 296 | return 0 |
| 297 | } |
| 298 | |
| 299 | # ============================================ |
| 300 | # Run |
| 301 | # ============================================ |
| 302 | |
| 303 | function cmd::inspect::run() { |
| 304 | local name="" type="" show_config=false show_qr=false |
| 305 | |
| 306 | if [[ $# -gt 0 && "$1" != "--"* ]]; then |
| 307 | name="$1" |
| 308 | shift |
| 309 | fi |
| 310 | |
| 311 | while [[ $# -gt 0 ]]; do |
| 312 | case "$1" in |
| 313 | --name) name="$2"; shift 2 ;; |
| 314 | --type) type="$2"; shift 2 ;; |
| 315 | --config) show_config=true; shift ;; |
| 316 | --qr) show_qr=true; shift ;; |
| 317 | --help) cmd::inspect::help; return ;; |
| 318 | *) |
| 319 | log::error "Unknown flag: $1" |
| 320 | cmd::inspect::help |
| 321 | return 1 |
| 322 | ;; |
| 323 | esac |
| 324 | done |
| 325 | |
| 326 | if [[ -z "$name" ]]; then |
| 327 | log::error "Missing required flag: --name" |
| 328 | cmd::inspect::help |
| 329 | return 1 |
| 330 | fi |
| 331 | |
| 332 | name=$(peers::resolve_and_require "$name" "$type") || return 1 |
| 333 | |
| 334 | if command::json; then |
| 335 | cmd::inspect::_output_json "$name" |
| 336 | return 0 |
| 337 | fi |
| 338 | |
| 339 | load_command list |
| 340 | |
| 341 | log::section "Inspect: ${name}" |
| 342 | |
| 343 | cmd::inspect::_peer_info "$name" |
| 344 | cmd::inspect::_group_info "$name" |
| 345 | cmd::inspect::_rule_info "$name" |
| 346 | cmd::inspect::_blocks_info "$name" |
| 347 | cmd::inspect::_firewall_info "$name" |
| 348 | |
| 349 | if $show_config; then |
| 350 | cmd::inspect::_config "$name" |
| 351 | fi |
| 352 | |
| 353 | if $show_qr; then |
| 354 | cmd::inspect::_section "QR Code" |
| 355 | printf "\n" |
| 356 | load_command qr |
| 357 | cmd::qr::run --name "$name" |
| 358 | fi |
| 359 | |
| 360 | printf "\n" |
| 361 | } |
| 362 | |
| 363 | # ============================================ |
| 364 | # JSON (API consumption) |
| 365 | # ============================================ |
| 366 | |
| 367 | function cmd::inspect::_output_json() { |
| 368 | local name="${1:-}" |
| 369 | |
| 370 | local ip type rule allowed_ips public_key is_blocked status |
| 371 | ip=$(peers::get_ip "$name") |
| 372 | type=$(peers::get_type "$name") |
| 373 | rule=$(peers::get_meta "$name" "rule") |
| 374 | allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" 2>/dev/null | \ |
| 375 | awk '{print $3}' | tr -d ',') |
| 376 | public_key=$(keys::public "$name" 2>/dev/null || echo "") |
| 377 | peers::is_blocked "$name" && is_blocked="true" || is_blocked="false" |
| 378 | |
| 379 | # Handshake status |
| 380 | local handshake_ts=0 |
| 381 | handshake_ts=$(wg show "$(config::interface)" latest-handshakes 2>/dev/null | \ |
| 382 | grep "$public_key" | awk '{print $2}') || handshake_ts=0 |
| 383 | |
| 384 | local last_ts |
| 385 | last_ts=$(peers::get_meta "$name" "last_ts" 2>/dev/null || echo "") |
| 386 | |
| 387 | local conn_state |
| 388 | conn_state=$(peers::connection_state "$is_blocked" "false" \ |
| 389 | "${handshake_ts:-0}" "${last_ts:-}" | cut -d'|' -f1) |
| 390 | |
| 391 | # Groups |
| 392 | local groups_json="[]" |
| 393 | local -a group_list=() |
| 394 | while IFS= read -r g; do |
| 395 | [[ -n "$g" ]] && group_list+=("\"$g\"") |
| 396 | done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null) |
| 397 | [[ ${#group_list[@]} -gt 0 ]] && \ |
| 398 | groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]" |
| 399 | |
| 400 | # Identity |
| 401 | local identity |
| 402 | identity=$(peers::get_identity "$name" 2>/dev/null || echo "") |
| 403 | |
| 404 | # Rule extends |
| 405 | local rule_extends="[]" |
| 406 | if [[ -n "$rule" ]]; then |
| 407 | local rule_file |
| 408 | rule_file=$(json::find_rule_file "$(ctx::rules)" "$rule" 2>/dev/null) |
| 409 | if [[ -n "$rule_file" ]]; then |
| 410 | local -a extends=() |
| 411 | while IFS= read -r ext; do |
| 412 | [[ -n "$ext" ]] && extends+=("\"$ext\"") |
| 413 | done < <(json::get "$rule_file" "extends" 2>/dev/null) |
| 414 | [[ ${#extends[@]} -gt 0 ]] && \ |
| 415 | rule_extends="[$(printf '%s,' "${extends[@]}" | sed 's/,$//')]" |
| 416 | fi |
| 417 | fi |
| 418 | |
| 419 | local data |
| 420 | data=$(printf '{"name":"%s","ip":"%s","type":"%s","rule":"%s","rule_extends":%s,"allowed_ips":"%s","public_key":"%s","is_blocked":%s,"status":"%s","identity":"%s","groups":%s}' \ |
| 421 | "$name" "$ip" "$type" \ |
| 422 | "${rule:-}" "$rule_extends" \ |
| 423 | "${allowed_ips:-}" "$public_key" \ |
| 424 | "$is_blocked" "$conn_state" \ |
| 425 | "${identity:-}" "$groups_json") |
| 426 | |
| 427 | printf '%s' "$data" | json::envelope "inspect" "1" |
| 428 | } |