Naposledy aktivní 1 month ago

nuno revidoval tento gist 1 month ago. Přejít na revizi

1 file changed, 560 insertions

gistfile1.txt(vytvořil soubor)

@@ -0,0 +1,560 @@
1 + #!/usr/bin/env bash
2 +
3 + # ============================================
4 + # Lifecycle
5 + # ============================================
6 +
7 + function 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 +
22 + function cmd::list::help() {
23 + cat <<EOF
24 + Usage: wgctl list [options]
25 +
26 + List all WireGuard clients.
27 +
28 + Options:
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 +
38 + Examples:
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
47 + EOF
48 + }
49 +
50 + # ============================================
51 + # Status Helpers
52 + # ============================================
53 +
54 + function 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 +
61 + function 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 +
69 + function 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 +
80 + function 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 "
89 + from datetime import datetime, timezone
90 + dt = datetime.fromisoformat('${ts}')
91 + if dt.tzinfo is None:
92 + dt = dt.replace(tzinfo=timezone.utc)
93 + print(int(dt.timestamp()))
94 + " 2>/dev/null || echo 0)
95 +
96 + diff=$(( now - attempt_ts ))
97 + (( diff < 180 ))
98 + }
99 +
100 + function cmd::list::is_blocked() {
101 + local name="$1"
102 + peers::is_blocked "$name"
103 + }
104 +
105 + function cmd::list::is_restricted() {
106 + local name="$1"
107 + [[ -f "$(ctx::block::path "${name}.block")" ]]
108 + }
109 +
110 + function 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 "
122 + from datetime import datetime, timezone
123 + dt = datetime.fromisoformat('${ts}')
124 + print(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 +
143 + function 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 +
189 + function 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 +
213 + function 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 +
267 + if [[ -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 +
311 + function 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 +
507 + local group_summary=""
508 + if $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%, }"
518 + fi
519 +
520 + cmd::list::_render_summary "$group_summary"
521 +
522 + printf "\n"
523 + }
524 +
525 + function 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
553 + function 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 + }
Novější Starší