Ultima attività 1 month ago

gistfile1.txt Raw
1#!/usr/bin/env bash
2
3# ============================================
4# Lifecycle
5# ============================================
6
7function cmd::list::on_load() {
8 flag::register --type
9 flag::register --online
10 flag::register --offline
11 flag::register --restricted
12 flag::register --blocked
13 flag::register --allowed
14 flag::register --detailed
15 flag::register --name
16}
17
18# ============================================
19# Help
20# ============================================
21
22function cmd::list::help() {
23 cat <<EOF
24Usage: wgctl list [options]
25
26List all WireGuard clients.
27
28Options:
29 --type <type> Filter by device type
30 --online Show only connected clients
31 --offline Show only disconnected clients
32 --allowed Show only fully allowed clients
33 --restricted Show only restricted clients
34 --blocked Show only blocked clients
35 --detailed Show full detail cards for all clients
36 --name <name> Show detail card for a single client
37
38Examples:
39 wgctl list
40 wgctl ls --type phone
41 wgctl list --online
42 wgctl list --blocked
43 wgctl list --allowed
44 wgctl list --restricted
45 wgctl list --detailed
46 wgctl list --name phone-nuno
47EOF
48}
49
50# ============================================
51# Status Helpers
52# ============================================
53
54function cmd::list::last_handshake_ts() {
55 local public_key="$1"
56 wg show "$(config::interface)" latest-handshakes 2>/dev/null \
57 | grep "^${public_key}" \
58 | awk '{print $2}'
59}
60
61function cmd::list::last_dropped_ts() {
62 local client_ip="$1"
63 journalctl -k --grep "wgctl-dropped: " 2>/dev/null \
64 | grep "SRC=${client_ip}" \
65 | tail -1 \
66 | awk '{print $1, $2, $3}'
67}
68
69function cmd::list::is_connected() {
70 local public_key="$1"
71 local ts
72 ts=$(cmd::list::last_handshake_ts "$public_key")
73 [[ -z "$ts" || "$ts" == "0" ]] && return 1
74 local now diff
75 now=$(date +%s)
76 diff=$(( now - ts ))
77 (( diff < 180 ))
78}
79
80function cmd::list::is_attempting() {
81 local name="$1"
82 local ts
83 ts=$(monitor::last_attempt "$name")
84 [[ -z "$ts" ]] && return 1
85
86 local now attempt_ts diff
87 now=$(date +%s)
88 attempt_ts=$(python3 -c "
89from datetime import datetime, timezone
90dt = datetime.fromisoformat('${ts}')
91if dt.tzinfo is None:
92 dt = dt.replace(tzinfo=timezone.utc)
93print(int(dt.timestamp()))
94" 2>/dev/null || echo 0)
95
96 diff=$(( now - attempt_ts ))
97 (( diff < 180 ))
98}
99
100function cmd::list::is_blocked() {
101 local name="$1"
102 peers::is_blocked "$name"
103}
104
105function cmd::list::is_restricted() {
106 local name="$1"
107 [[ -f "$(ctx::block::path "${name}.block")" ]]
108}
109
110function cmd::list::format_last_seen() {
111 local name="$1"
112 local public_key="$2"
113 local ip="$3"
114
115 if cmd::list::is_blocked "$name"; then
116 local ts
117 ts=$(monitor::last_attempt "$name")
118 if [[ -n "$ts" ]]; then
119 # Format ISO timestamp
120 local formatted
121 formatted=$(python3 -c "
122from datetime import datetime, timezone
123dt = datetime.fromisoformat('${ts}')
124print(dt.strftime('%Y-%m-%d %H:%M'))
125" 2>/dev/null || echo "$ts")
126 echo "${formatted} (dropped)"
127 else
128 echo "—"
129 fi
130 else
131 local ts
132 ts=$(cmd::list::last_handshake_ts "$public_key")
133 if [[ -z "$ts" || "$ts" == "0" ]]; then
134 echo "—"
135 else
136 local formatted
137 formatted=$(date -d "@${ts}" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$ts")
138 echo "${formatted} (handshake)"
139 fi
140 fi
141}
142
143function cmd::list::format_status() {
144 local name="$1"
145 local public_key="$2"
146 local ip="$3" # new
147
148 local connected=false
149 local blocked=false
150 local restricted=false
151
152 cmd::list::is_blocked "$name" && blocked=true
153 cmd::list::is_restricted "$name" && restricted=true
154
155 if $blocked; then
156 cmd::list::is_attempting "$name" && connected=true
157 modifier=" (blocked)"
158 elif $restricted; then
159 cmd::list::is_connected "$public_key" && connected=true
160 modifier=" (restricted)"
161 else
162 cmd::list::is_connected "$public_key" && connected=true
163 modifier=""
164 fi
165
166 local conn_str
167 $connected && conn_str="online" || conn_str="offline"
168
169 local status="${conn_str}${modifier}"
170
171 local color
172 if $blocked; then
173 color="\033[1;31m"
174 elif $restricted; then
175 color="\033[1;33m"
176 elif $connected; then
177 color="\033[1;32m"
178 else
179 color="\033[0;37m"
180 fi
181
182 echo -e "${color}${status}\033[0m"
183}
184
185# ============================================
186# Display Type
187# ============================================
188
189function cmd::list::display_type() {
190 local name="$1"
191 local type="$2"
192
193 # log::debug "$(config::is_guest_type "$type")"
194 if config::is_guest_type "$type"; then
195 local subtype
196 subtype=$(peers::get_meta "$name" "subtype")
197 # log::debug "$subtype"
198
199 if [[ -n "$subtype" ]]; then
200 echo "guest/${subtype}"
201 else
202 echo "guest"
203 fi
204 else
205 echo "$type"
206 fi
207}
208
209# ============================================
210# Detail Card
211# ============================================
212
213function cmd::list::show_client() {
214 local name="$1"
215 local dir
216 dir="$(ctx::clients)"
217 local conf="${dir}/${name}.conf"
218
219 if [[ ! -f "$conf" ]]; then
220 log::error "Client not found: ${name}"
221 return 1
222 fi
223
224 local ip
225 ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
226
227 local allowed_ips
228 allowed_ips=$(grep "^AllowedIPs" "$conf" | awk '{print $3}')
229
230 local public_key
231 public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
232
233 # Get endpoint
234 local endpoint="—"
235 if cmd::list::is_blocked "$name"; then
236 local ep
237 ep=$(monitor::last_endpoint "$name")
238 [[ -n "$ep" ]] && endpoint="$ep"
239 else
240 local ep
241 ep=$(monitor::endpoint_for_key "$public_key")
242 [[ -n "$ep" ]] && endpoint="$ep"
243 fi
244
245 # Determine type
246 local type="unknown"
247 for t in $(config::device_types); do
248 local subnet
249 subnet=$(config::subnet_for "$t")
250 if string::starts_with "$ip" "$subnet."; then
251 type="$t"
252 break
253 fi
254 done
255
256 local status
257 status=$(cmd::list::format_status "$name" "$public_key" "$ip")
258
259 local last_seen
260 last_seen=$(cmd::list::format_last_seen "$name" "$public_key" "$ip")
261
262 # Block rules
263 local block_file
264 block_file="$(ctx::block::path "${name}.block")"
265 local blocks=""
266
267if [[ -f "$block_file" ]] && [[ -s "$block_file" ]]; then
268 while IFS=" " read -r client_ip target port proto; do
269 if [[ -z "$target" ]]; then
270 blocks+=" all traffic blocked\n"
271 else
272 local rule=" ${target}"
273 [[ -n "$port" ]] && rule+=":${port}/${proto}"
274 blocks+="${rule}\n"
275 fi
276 done < "$block_file"
277 fi
278
279 local sep
280 sep="$(printf '─%.0s' {1..50})"
281
282 echo ""
283 echo " ${sep}"
284 printf " \033[1;34m%-20s\033[0m %s\n" "Client:" "$name"
285 echo " ${sep}"
286 printf " %-20s %s\n" "IP:" "$ip"
287 printf " %-20s %s\n" "Type:" "$type"
288 printf " %-20s %b\n" "Status:" "$status"
289 printf " %-20s %s\n" "Endpoint:" "$endpoint"
290 printf " %-20s %s\n" "Last seen:" "$last_seen"
291 printf " %-20s %s\n" "Allowed IPs:" "$allowed_ips"
292 printf " %-20s %s\n" "Public key:" "$public_key"
293
294 if [[ -z "$blocks" ]]; then
295 printf " %-20s %s\n" "Blocks:" "none"
296 elif [[ "$blocks" == *"all traffic blocked"* ]]; then
297 printf " %-20s \033[1;31mAll\033[0m\n" "Blocks:"
298 else
299 printf " %-20s\n" "Blocks:"
300 echo -e "$blocks"
301 fi
302
303 echo " ${sep}"
304 echo ""
305}
306
307# ============================================
308# Run
309# ============================================
310
311function cmd::list::run() {
312 local filter_type=""
313 local online_only=false
314 local offline_only=false
315 local restricted_only=false
316 local blocked_only=false
317 local allowed_only=false
318 local detailed=false
319 local single_name=""
320
321 while [[ $# -gt 0 ]]; do
322 case "$1" in
323 --type) filter_type="$2"; shift 2 ;;
324 --online) online_only=true; shift ;;
325 --offline) offline_only=true; shift ;;
326 --restricted) restricted_only=true; shift ;;
327 --blocked) blocked_only=true; shift ;;
328 --allowed) allowed_only=true; shift ;;
329 --detailed) detailed=true; shift ;;
330 --name) single_name="$2"; shift 2 ;;
331 --help) cmd::list::help; return ;;
332 *)
333 log::error "Unknown flag: $1"
334 cmd::list::help
335 return 1
336 ;;
337 esac
338 done
339
340 # Single client detail card
341 if [[ -n "$single_name" ]]; then
342 cmd::list::show_client "$single_name"
343 return
344 fi
345
346 local dir
347 dir="$(ctx::clients)"
348 local confs=("${dir}"/*.conf)
349
350 if [[ ! -f "${confs[0]}" ]]; then
351 log::wg_list "No clients configured"
352 return 0
353 fi
354
355 # START - GROUP SECTION
356 # Check if any groups exist
357 local has_groups=false
358 local groups_dir
359 groups_dir="$(ctx::groups)"
360 local group_files=("${groups_dir}"/*.group)
361 [[ -f "${group_files[0]}" ]] && has_groups=true
362
363 # Precompute peer->group map
364 declare -A peer_group_map
365 if $has_groups; then
366 while IFS=":" read -r peer_name group_name; do
367 [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
368 done < <(json::peer_group_map "$groups_dir")
369 fi
370 # END - GROUP SECTION
371
372 # Detailed mode — cards only, no table
373 if $detailed; then
374 log::section "WireGuard Clients"
375 for conf in "${dir}"/*.conf; do
376 [[ -f "$conf" ]] || continue
377 local client_name
378 client_name=$(basename "$conf" .conf)
379
380 # Apply type filter
381 if [[ -n "$filter_type" ]]; then
382 local ip
383 ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
384 local type="unknown"
385 for t in $(config::device_types); do
386 local subnet
387 subnet=$(config::subnet_for "$t")
388 if string::starts_with "$ip" "$subnet."; then
389 type="$t"
390 break
391 fi
392 done
393 [[ "$type" != "$filter_type" ]] && continue
394 fi
395
396 cmd::list::show_client "$client_name"
397 done
398 return
399 fi
400
401 # Normal table view
402 log::section "WireGuard Clients"
403
404 if $has_groups; then
405 printf "\n %-28s %-15s %-13s %-12s %-12s %-22s %s\n" \
406 "NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
407 printf " %s\n" "$(printf '─%.0s' {1..135})"
408 else
409 printf "\n %-28s %-15s %-13s %-12s %-22s %s\n" \
410 "NAME" "IP" "TYPE" "RULE" "STATUS" "LAST SEEN"
411 printf " %s\n" "$(printf '─%.0s' {1..107})"
412 fi
413
414 for conf in "${dir}"/*.conf; do
415 [[ -f "$conf" ]] || continue
416
417 local client_name
418 client_name=$(basename "$conf" .conf)
419
420 local ip
421 ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
422
423 # Determine type
424 local type="unknown"
425 for t in $(config::device_types); do
426 local subnet
427 subnet=$(config::subnet_for "$t")
428 if string::starts_with "$ip" "$subnet."; then
429 type="$t"
430 break
431 fi
432 done
433
434 # Apply type filter
435 if [[ -n "$filter_type" && "$type" != "$filter_type" ]]; then
436 continue
437 fi
438
439 local public_key
440 public_key=$(keys::public "$client_name" 2>/dev/null || echo "")
441
442 # Apply filters
443 if $online_only && ! cmd::list::is_connected "$public_key"; then
444 continue
445 fi
446
447 if $offline_only && cmd::list::is_connected "$public_key"; then
448 continue
449 fi
450
451 if $restricted_only && ! cmd::list::is_restricted "$client_name"; then
452 continue
453 fi
454
455 if $blocked_only && ! cmd::list::is_blocked "$client_name"; then
456 continue
457 fi
458
459 if $allowed_only && { cmd::list::is_blocked "$client_name" || cmd::list::is_restricted "$client_name"; }; then
460 continue
461 fi
462
463 local status
464 status=$(cmd::list::format_status "$client_name" "$public_key" "$ip")
465
466 local last_seen
467 last_seen=$(cmd::list::format_last_seen "$client_name" "$public_key" "$ip")
468
469 local display_type
470 display_type=$(cmd::list::display_type "$client_name" "$type")
471 # log::debug "display_type called with name=$client_name type=$type"
472
473 local rule
474 rule=$(peers::effective_rule "$client_name")
475 rule="${rule:-—}"
476
477 local padded_status
478 padded_status=$(cmd::list::pad_status "$status" 25)
479 local group_display="—"
480 if $has_groups; then
481 group_display="${peer_group_map[$client_name]:-—}"
482 fi
483
484 local rule_col_width=12
485 [[ "$rule" == "—" ]] && rule_col_width=14
486
487 local group_col_width=12
488 [[ "$group_display" == "—" ]] && group_col_width=14
489
490 if $has_groups; then
491 printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
492 "$client_name" "$ip" "$display_type" "$rule" \
493 "$group_display" "$padded_status" "$last_seen"
494 else
495 printf " %-28s %-15s %-13s %-12s %s %s\n" \
496 "$client_name" "$ip" "$display_type" "$rule" \
497 "$padded_status" "$last_seen"
498 fi
499 done
500
501 if $has_groups; then
502 printf " %s\n" "$(printf '─%.0s' {1..135})"
503 else
504 printf " %s\n" "$(printf '─%.0s' {1..107})"
505 fi
506
507local group_summary=""
508if $has_groups; then
509 declare -A group_counts
510 for peer in "${!peer_group_map[@]}"; do
511 local g="${peer_group_map[$peer]}"
512 group_counts["$g"]=$(( ${group_counts["$g"]:-0} + 1 )) || true
513 done
514 for g in "${!group_counts[@]}"; do
515 group_summary+="${group_counts[$g]} in ${g}, "
516 done
517 group_summary="${group_summary%, }"
518fi
519
520 cmd::list::_render_summary "$group_summary"
521
522 printf "\n"
523}
524
525function cmd::list::_render_summary() {
526 local group_summary="${1:-}"
527 # Summary line
528 local total online_count
529 total=$(peers::all | wc -l)
530
531 # Count by rule
532 declare -A rule_summary
533 while IFS= read -r peer_name; do
534 local r
535 r=$(peers::effective_rule "$peer_name")
536 rule_summary["$r"]=$(( ${rule_summary["$r"]:-0} + 1 ))
537 done < <(peers::all)
538
539 local summary=""
540 for r in "${!rule_summary[@]}"; do
541 summary+="${rule_summary[$r]} ${r}, "
542 done
543 summary="${summary%, }" # remove trailing comma
544
545 if [[ -n "$group_summary" ]]; then
546 printf "\n Showing %s peers [%s] — %s\n\n" "$total" "$summary" "$group_summary"
547 else
548 printf "\n Showing %s peers [%s]\n\n" "$total" "$summary"
549 fi
550}
551
552# Strip ANSI codes to measure visible length, then pad manually
553function cmd::list::pad_status() {
554 local status="$1"
555 local width="${2:-20}"
556 local visible
557 visible=$(echo -e "$status" | sed 's/\x1b\[[0-9;]*m//g')
558 local pad=$(( width - ${#visible} ))
559 printf "%b%${pad}s" "$status" ""
560}