Последняя активность 1 month ago

nuno ревизий этого фрагмента 1 month ago. К ревизии

1 file changed, 428 insertions

inspect.command.sh(файл создан)

@@ -0,0 +1,428 @@
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 + }
Новее Позже