Naposledy aktivní 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 --rule
10 flag::register --group
11 flag::register --online
12 flag::register --offline
13 flag::register --restricted
14 flag::register --blocked
15 flag::register --allowed
16 flag::register --detailed
17 flag::register --name
18}
19
20# ============================================
21# Help
22# ============================================
23
24function cmd::list::help() {
25 cat <<EOF
26Usage: wgctl list [options]
27
28List all WireGuard clients.
29
30Options:
31 --type <type> Filter by device type (desktop, laptop, phone, tablet)
32 --rule <rule> Filter by assigned rule
33 --group <group> Filter by group membership
34 --online Show only connected clients
35 --offline Show only disconnected clients
36 --blocked Show only fully blocked clients (removed from WireGuard)
37 --restricted Show only restricted clients (specific IP/port blocks applied)
38 --allowed Show only unrestricted clients
39 --detailed Show full detail cards for all clients
40 --name <name> Show detail card for a single client
41
42Status values:
43 online Connected (recent handshake)
44 offline Not connected
45 blocked Removed from WireGuard server (wgctl block --name)
46 restricted In WireGuard but with specific access rules (wgctl block --ip/--service)
47
48Examples:
49 wgctl list
50 wgctl list --type phone
51 wgctl list --rule user
52 wgctl list --group family
53 wgctl list --online
54 wgctl list --blocked
55 wgctl list --restricted
56 wgctl list --detailed
57 wgctl list --name phone-nuno
58EOF
59}
60
61# ============================================
62# Header / Footer
63# ============================================
64
65function cmd::list::_render_header() {
66 local has_groups="$1"
67 if $has_groups; then
68 printf "\n %-28s %-15s %-13s %-12s %-12s %-22s %s\n" \
69 "NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
70 printf " %s\n" "$(printf '─%.0s' {1..135})"
71 else
72 printf "\n %-28s %-15s %-13s %-12s %-22s %s\n" \
73 "NAME" "IP" "TYPE" "RULE" "STATUS" "LAST SEEN"
74 printf " %s\n" "$(printf '─%.0s' {1..107})"
75 fi
76}
77
78function cmd::list::_render_footer() {
79 local has_groups="$1"
80 if $has_groups; then
81 printf " %s\n" "$(printf '─%.0s' {1..135})"
82 else
83 printf " %s\n" "$(printf '─%.0s' {1..107})"
84 fi
85}
86
87function cmd::list::_render_summary() {
88 local group_summary="${1:-}"
89 local -n _rule_counts="$2"
90 local filter_desc="${3:-}"
91
92 local total=0
93 for r in "${!_rule_counts[@]}"; do
94 (( total += _rule_counts[$r] )) || true
95 done
96
97 local summary=""
98 for r in "${!_rule_counts[@]}"; do
99 summary+="${_rule_counts[$r]} ${r}, "
100 done
101 summary="${summary%, }"
102
103 if [[ -n "$group_summary" ]]; then
104 printf "\n Showing %s peers [%s] — %s\n\n" "$total" "$summary" "$group_summary"
105 else
106 printf "\n Showing %s peers [%s]\n\n" "$total" "$summary"
107 fi
108}
109
110# ============================================
111# Detail Card
112# ============================================
113
114function cmd::list::show_client() {
115 local name="${1:-}"
116 local conf
117 conf="$(ctx::clients)/${name}.conf"
118
119 if [[ ! -f "$conf" ]]; then
120 log::error "Client not found: ${name}"
121 return 1
122 fi
123
124 local ip
125 ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
126
127 local allowed_ips
128 allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs)
129
130 local public_key
131 public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
132
133 # Meta type is authoritative; IP reverse lookup is fallback for pre-migration peers
134 local type
135 type=$(peers::get_meta "$name" "type" 2>/dev/null)
136 [[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
137
138 local endpoint="—"
139 local ep
140 ep=$(monitor::endpoint_for_key "$public_key")
141 [[ -z "$ep" ]] && ep=$(monitor::last_endpoint "$name")
142 [[ -n "$ep" ]] && endpoint="$ep"
143
144 local is_blocked="false"
145 peers::is_blocked "$name" && is_blocked="true"
146
147 local is_restricted="false"
148 peers::is_restricted "$name" && is_restricted="true"
149
150 local handshake_ts
151 handshake_ts=$(monitor::get_handshake_ts "$public_key")
152
153 local last_ts
154 last_ts=$(monitor::last_attempt "$name")
155
156 local status
157 status=$(peers::format_status_verbose "$name" "$public_key" \
158 "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
159
160 local last_seen
161 last_seen=$(peers::format_last_seen "$name" "$public_key" \
162 "$is_blocked" "$last_ts" "" "$handshake_ts")
163
164 local blocks=""
165 if block::has_file "$name" && block::is_blocked "$name"; then
166 if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then
167 blocks="all traffic blocked"
168 fi
169 local rule_lines
170 rule_lines=$(block::format_rules "$name")
171 [[ -n "$rule_lines" ]] && blocks+="$rule_lines"
172 fi
173
174 ui::section "Client: ${name}"
175 ui::row "IP" "$ip"
176 ui::row "Type" "$(peers::display_type "$type")"
177 ui::row "Status" "$(echo -e "$status")"
178 ui::row "Endpoint" "$endpoint"
179 ui::row "Last seen" "$last_seen"
180 ui::row "AllowedIPs" "$allowed_ips"
181 ui::row "Public key" "$public_key"
182
183 if [[ -z "$blocks" ]]; then
184 ui::row "Blocks" "none"
185 elif [[ "$blocks" == *"all traffic blocked"* ]]; then
186 ui::row "Blocks" "$(echo -e "\033[1;31mAll\033[0m")"
187 else
188 printf " %-20s\n" "Blocks:"
189 echo -e "$blocks"
190 fi
191 printf "\n"
192}
193
194# ============================================
195# Run
196# ============================================
197
198function cmd::list::run() {
199 local filter_type="" filter_rule="" filter_group=""
200 local online_only=false offline_only=false
201 local restricted_only=false blocked_only=false allowed_only=false
202 local detailed=false single_name=""
203
204 while [[ $# -gt 0 ]]; do
205 case "$1" in
206 --type) filter_type="$2"; shift 2 ;;
207 --rule) filter_rule="$2"; shift 2 ;;
208 --group) filter_group="$2"; shift 2 ;;
209 --online) online_only=true; shift ;;
210 --offline) offline_only=true; shift ;;
211 --restricted) restricted_only=true; shift ;;
212 --blocked) blocked_only=true; shift ;;
213 --allowed) allowed_only=true; shift ;;
214 --detailed) detailed=true; shift ;;
215 --name) single_name="$2"; shift 2 ;;
216 --help) cmd::list::help; return ;;
217 *)
218 log::error "Unknown flag: $1"
219 cmd::list::help
220 return 1
221 ;;
222 esac
223 done
224
225 if [[ -n "$single_name" ]]; then
226 cmd::list::show_client "$single_name"
227 return 0
228 fi
229
230 local dir
231 dir="$(ctx::clients)"
232 local confs=("${dir}"/*.conf)
233 if [[ ! -f "${confs[0]}" ]]; then
234 log::wg_warning "No clients configured"
235 return 0
236 fi
237
238 cmd::list::_precompute_all
239
240 if $detailed; then
241 log::section "WireGuard Clients"
242 cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
243 return 0
244 fi
245
246 local filter_desc=""
247 cmd::list::_build_filter_desc
248
249 declare -A rule_counts=() group_counts=()
250 _list_header_printed=false
251
252 cmd::list::_iter_confs "$filter_type" cmd::list::_render_row
253
254 if [[ "$_list_header_printed" == "true" ]]; then
255 cmd::list::_render_footer $has_groups
256 local group_summary=""
257 cmd::list::_build_group_summary
258 cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc"
259 else
260 log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}"
261 fi
262}
263
264# ============================================
265# Iteration
266# ============================================
267
268function cmd::list::_iter_confs() {
269 local filter_type="$1" callback="$2"
270 local dir
271 dir="$(ctx::clients)"
272
273 for conf in "${dir}"/*.conf; do
274 [[ -f "$conf" ]] || continue
275 local client_name
276 client_name=$(basename "$conf" .conf)
277 local ip="${p_ips[$client_name]:-}"
278 [[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
279
280 # p_types is authoritative — set during precompute from meta + IP fallback
281 local type="${p_types[$client_name]:-unknown}"
282 [[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
283
284 "$callback" "$client_name" "$ip" "$type"
285 done
286}
287
288# ============================================
289# Row rendering
290# ============================================
291
292function cmd::list::_render_row() {
293 local client_name="$1" ip="$2" type="$3"
294
295 local pubkey="${p_pubkeys[$client_name]:-}"
296 local handshake_ts="${wg_handshakes[$pubkey]:-0}"
297 local is_blocked="${p_blocked[$client_name]:-false}"
298 local is_restricted="${p_restricted[$client_name]:-false}"
299 local last_ts="${p_last_ts[$client_name]:-}"
300
301 if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
302 if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
303 if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
304 if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
305 if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
306 [[ "$is_restricted" == "true" ]]; }; then return 0; fi
307
308 if [[ -n "$filter_group" ]]; then
309 local all_groups="${peer_group_map[$client_name]:-}"
310 [[ "$all_groups" != *"$filter_group"* ]] && return 0
311 fi
312
313 local status last_seen display_type rule group_display
314 status=$(peers::format_status_verbose "$client_name" "$pubkey" \
315 "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
316 last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
317 "$is_blocked" "$last_ts" "" "$handshake_ts")
318 display_type=$(peers::display_type "$type")
319 rule="${p_rules[$client_name]:-—}"
320
321 if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
322
323 if [[ "${_list_header_printed:-false}" == "false" ]]; then
324 log::section "WireGuard Clients"
325 cmd::list::_render_header $has_groups
326 _list_header_printed=true
327 fi
328
329 rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
330
331 local padded_status
332 padded_status=$(ui::pad_status "$status" 25)
333
334 if $has_groups; then
335 local main_group="${p_main_groups[$client_name]:-}"
336 if [[ -n "$main_group" ]]; then
337 group_display="$main_group"
338 else
339 group_display="${peer_group_map[$client_name]:-—}"
340 fi
341
342 if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
343 group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
344 fi
345
346 local rule_col_width=12 group_col_width=12
347 [[ "$rule" == "—" ]] && rule_col_width=14
348 [[ "$group_display" == "—" ]] && group_col_width=14
349 printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
350 "$client_name" "$ip" "$display_type" "$rule" \
351 "$group_display" "$padded_status" "$last_seen"
352 else
353 local rule_col_width=12
354 [[ "$rule" == "—" ]] && rule_col_width=14
355 printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \
356 "$client_name" "$ip" "$display_type" "$rule" \
357 "$padded_status" "$last_seen"
358 fi
359}
360
361# ============================================
362# Precompute
363# ============================================
364
365function cmd::list::_precompute_all() {
366 # Peer data — field 4 is 'type' from peer_data_v2
367 declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=()
368 while IFS="|" read -r name ip rule type last_ts last_evt main_group; do
369 [[ -z "$name" ]] && continue
370 p_ips["$name"]="$ip"
371 p_rules["$name"]="${rule:-—}"
372 p_types["$name"]="${type:-}"
373 p_last_ts["$name"]="$last_ts"
374 p_last_evt["$name"]="$last_evt"
375 p_main_groups["$name"]="${main_group:-}"
376 done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
377
378 # Fill type from IP for peers missing meta type (pre-migration peers)
379 for name in "${!p_ips[@]}"; do
380 [[ -n "${p_types[$name]:-}" ]] && continue
381 p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
382 done
383
384 # WireGuard handshakes + endpoints
385 declare -gA wg_handshakes=() wg_endpoints=()
386 while IFS=$'\t' read -r pubkey ts; do
387 [[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
388 done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
389 while IFS=$'\t' read -r pubkey endpoint; do
390 [[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
391 done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
392
393 # Block/restricted status
394 declare -gA p_blocked=() p_restricted=()
395 cmd::list::_precompute_block_status p_blocked p_restricted
396
397 # Public keys
398 declare -gA p_pubkeys=()
399 local dir
400 dir="$(ctx::clients)"
401 for kf in "${dir}"/*_public.key; do
402 [[ -f "$kf" ]] || continue
403 local kname
404 kname=$(basename "$kf" _public.key)
405 p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
406 done
407
408 # Groups + main group
409 has_groups=false
410 declare -gA peer_group_map=()
411 local groups_dir
412 groups_dir="$(ctx::groups)"
413 local group_files=("${groups_dir}"/*.group)
414 if [[ -f "${group_files[0]}" ]]; then
415 has_groups=true
416 while IFS=":" read -r peer_name group_name; do
417 [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
418 done < <(json::peer_group_map "$groups_dir")
419 fi
420
421 # Transfer/activity data — keyed by pubkey
422 declare -gA p_rx=() p_tx=() p_activity=()
423 while IFS="|" read -r pubkey rx tx level; do
424 [[ -z "$pubkey" ]] && continue
425 p_rx["$pubkey"]="$rx"
426 p_tx["$pubkey"]="$tx"
427 p_activity["$pubkey"]="$level"
428 done < <(json::peer_transfer "$(config::interface)")
429}
430
431function cmd::list::_precompute_block_status() {
432 local -n _blocked="$1"
433 local -n _restricted="$2"
434
435 local wg_peers
436 wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
437
438 while IFS= read -r name; do
439 if block::has_specific_rules "$name" 2>/dev/null; then
440 _restricted["$name"]=true
441 else
442 _restricted["$name"]=false
443 fi
444
445 local pubkey
446 pubkey=$(keys::public "$name" 2>/dev/null || echo "")
447 if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
448 _blocked["$name"]=true
449 else
450 _blocked["$name"]=false
451 fi
452 done < <(peers::all)
453}
454
455# ============================================
456# Filter helpers
457# ============================================
458
459function cmd::list::_build_filter_desc() {
460 filter_desc=""
461 [[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
462 [[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
463 [[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
464 $online_only && filter_desc+="online "
465 $offline_only && filter_desc+="offline "
466 $blocked_only && filter_desc+="blocked "
467 filter_desc="${filter_desc% }"
468}
469
470function cmd::list::_build_group_summary() {
471 group_summary=""
472 if $has_groups; then
473 declare -A _gs=()
474 for peer in "${!peer_group_map[@]}"; do
475 local g="${peer_group_map[$peer]}"
476 _gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true
477 done
478 for g in "${!_gs[@]}"; do
479 group_summary+="${_gs[$g]} in ${g}, "
480 done
481 group_summary="${group_summary%, }"
482 fi
483}
484
485function cmd::list::_show_client_safe() {
486 local name="$1"
487 cmd::list::show_client "$name" || true
488}