最終更新 1 month ago

nuno revised this gist 1 month ago. Go to revision

1 file changed, 488 insertions

gistfile1.txt(file created)

@@ -0,0 +1,488 @@
1 + #!/usr/bin/env bash
2 +
3 + # ============================================
4 + # Lifecycle
5 + # ============================================
6 +
7 + function 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 +
24 + function cmd::list::help() {
25 + cat <<EOF
26 + Usage: wgctl list [options]
27 +
28 + List all WireGuard clients.
29 +
30 + Options:
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 +
42 + Status 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 +
48 + Examples:
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
58 + EOF
59 + }
60 +
61 + # ============================================
62 + # Header / Footer
63 + # ============================================
64 +
65 + function 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 +
78 + function 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 +
87 + function 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 +
114 + function 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 +
198 + function 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 +
268 + function 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 +
292 + function 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 +
365 + function 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 +
431 + function 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 +
459 + function 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 +
470 + function 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 +
485 + function cmd::list::_show_client_safe() {
486 + local name="$1"
487 + cmd::list::show_client "$name" || true
488 + }
Newer Older