Dernière activité 1 month ago

Révision d1a9318c316d44a40166d6ffab35ccf8a6efe737

gistfile1.txt Brut
1#!/usr/bin/env bash
2
3# ============================================
4# Lifecycle
5# ============================================
6
7function cmd::watch::on_load() {
8 flag::register --type
9 flag::register --name
10 flag::register --peers
11 flag::register --blocked
12 flag::register --restricted
13 flag::register --allowed
14}
15
16# ============================================
17# Help
18# ============================================
19
20function cmd::watch::help() {
21 cat <<EOF
22Usage: wgctl watch [options]
23
24Live monitor of WireGuard activity. Shows:
25 - Handshakes from connected peers (green)
26 - Connection attempts from blocked peers (red, SOURCE: wg)
27 - Firewall drops from rule-restricted peers (red, SOURCE: fw)
28
29Options:
30 --name <name> Filter by client name
31 --type <type> Filter by device type
32 --peers <list> Comma-separated peer names (used internally by group watch)
33 --blocked Show only blocked peer attempts
34 --allowed Show only handshakes (allowed peers)
35 --restricted Show only firewall drop events
36
37Examples:
38 wgctl watch
39 wgctl watch --blocked
40 wgctl watch --allowed
41 wgctl watch --type phone
42 wgctl watch --name phone-nuno
43 wgctl watch --name phone-nuno --type phone
44EOF
45}
46
47# ============================================
48# Helpers
49# ============================================
50
51function cmd::watch::format_event() {
52 local ts="${1:-}" source="${2:-}" client="${3:-}"
53 local dest="${4:-}" event="${5:-}" status="${6:-}"
54
55 local event_color
56 case "$event" in
57 attempt|drop) event_color="\033[1;31m" ;;
58 handshake) event_color="\033[1;32m" ;;
59 *) event_color="\033[0;37m" ;;
60 esac
61
62 local status_color=""
63 case "$status" in
64 blocked) status_color="\033[1;31m" ;;
65 allowed) status_color="\033[1;32m" ;;
66 esac
67
68 printf " %-20s %-8s %-22s %-28s ${event_color}%-14s\033[0m ${status_color}%s\033[0m\n" \
69 "$ts" "$source" "$client" "${dest:-—}" "$event" "$status"
70}
71
72function cmd::watch::header() {
73 log::section "wgctl — Live Monitor (Ctrl+C to stop)"
74 printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \
75 "TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT" "STATUS"
76 printf " %s\n\n" "$(printf '─%.0s' {1..105})"
77}
78
79function cmd::watch::_peer_in_filter() {
80 local peer="$1"
81 shift
82 local peer_set=("$@")
83 [[ ${#peer_set[@]} -eq 0 ]] && return 0 # no filter = all pass
84 for p in "${peer_set[@]}"; do
85 [[ "$p" == "$peer" ]] && return 0
86 done
87 return 1
88}
89
90# ============================================
91# Handshake Poller
92# ============================================
93
94function cmd::watch::poll_handshakes() {
95 local filter_name="${1:-}" filter_type="${2:-}"
96 local allowed_only="${3:-false}"
97 local filter_peers="${4:-}"
98
99 local peer_set=()
100 [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
101
102 while IFS= read -r line; do
103 local public_key ts
104 public_key=$(echo "$line" | awk '{print $1}')
105 ts=$(echo "$line" | awk '{print $2}')
106
107 [[ -z "$ts" || "$ts" == "0" ]] && continue
108
109 # Find client by public key
110 local client_name=""
111 for conf in "$(ctx::clients)"/*.conf; do
112 [[ -f "$conf" ]] || continue
113 local name
114 name=$(basename "$conf" .conf)
115 local key
116 key=$(keys::public "$name" 2>/dev/null || echo "")
117 if [[ "$key" == "$public_key" ]]; then
118 client_name="$name"
119 break
120 fi
121 done
122
123 [[ -z "$client_name" ]] && continue
124
125 # Apply filters
126 [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue
127 cmd::watch::_peer_in_filter "$client_name" "${peer_set[@]}" || continue
128
129 if [[ -n "$filter_type" ]]; then
130 local ip
131 ip=$(grep "^Address" "$(ctx::clients)/${client_name}.conf" \
132 | awk '{print $3}' | cut -d'/' -f1)
133 local subnet
134 subnet=$(config::subnet_for "$filter_type")
135 string::starts_with "$ip" "$subnet" || continue
136 fi
137
138 # Only emit if handshake is new
139 local safe_key
140 safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1)
141 local prev_ts_file="/tmp/wgctl_hs_${safe_key}"
142 local prev_ts="0"
143 [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file")
144
145 if [[ "$ts" != "$prev_ts" ]]; then
146 echo "$ts" > "$prev_ts_file"
147
148 local formatted_ts
149 formatted_ts=$(fmt::datetime "$ts")
150
151 local endpoint
152 endpoint=$(monitor::endpoint_for_key "$public_key")
153
154 cmd::watch::format_event \
155 "$formatted_ts" "wg" "$client_name" "${endpoint:-—}" "handshake" "allowed"
156 fi
157
158 done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
159}
160
161# ============================================
162# Event Tailer
163# ============================================
164
165function cmd::watch::tail_events() {
166 local filter_name="${1:-}" filter_type="${2:-}"
167 local blocked_only="${3:-false}" restricted_only="${4:-false}"
168 local allowed_only="${5:-false}" filter_peers="${6:-}"
169
170 declare -A _WATCH_LAST_ATTEMPT=()
171 declare -A _WATCH_LAST_FW=()
172
173 local peer_set=()
174 [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
175
176 declare -A _WATCH_LAST_ATTEMPT=()
177
178 # Build ip->name map for fw events
179 declare -A ip_to_name=()
180 local conf_file
181 while IFS= read -r conf_file; do
182 local name
183 name=$(basename "$conf_file" .conf)
184 local ip
185 ip=$(grep "^Address" "$conf_file" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
186 [[ -n "$ip" && -n "$name" ]] && ip_to_name["$ip"]="$name"
187 done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
188
189 # Source tracker via temp file (persists across subshell iterations)
190 local source_file
191 source_file=$(mktemp)
192 echo "wg" > "$source_file"
193
194 # Cleanup temp file on exit
195 trap "rm -f '$source_file'" EXIT
196
197 tail -f "$(ctx::events_log)" "$(ctx::fw_events_log)" 2>/dev/null \
198 | while IFS= read -r line; do
199 [[ -z "$line" ]] && continue
200
201 # Handle tail -f file headers
202 if [[ "$line" == "==> "* ]]; then
203 if [[ "$line" == *"fw_events"* ]]; then
204 echo "fw" > "$source_file"
205 else
206 echo "wg" > "$source_file"
207 fi
208 continue
209 fi
210
211 local source
212 source=$(cat "$source_file")
213
214 if [[ "$source" == "wg" ]]; then
215 $allowed_only && continue # wg events are attempts/blocked
216
217 local event_data
218 event_data=$(json::parse_event "$line")
219 [[ -z "$event_data" ]] && continue
220
221 local ts client endpoint event
222 IFS="|" read -r ts client endpoint event <<< "$event_data"
223
224 [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
225 cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue
226
227 if [[ -n "$filter_type" ]]; then
228 local conf
229 conf="$(ctx::clients)/${client}.conf"
230 [[ -f "$conf" ]] || continue
231 local ip
232 ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
233 local subnet
234 subnet=$(config::subnet_for "$filter_type")
235 string::starts_with "$ip" "$subnet" || continue
236 fi
237
238 $restricted_only && { cmd::list::is_restricted "$client" || continue; }
239
240 # Dedup
241 local now
242 now=$(date +%s)
243 local safe_client="${client//[-.]/_}"
244 local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}"
245 (( now - last < 30 )) && continue
246 _WATCH_LAST_ATTEMPT[$safe_client]="$now"
247
248 local formatted_ts
249 formatted_ts=$(fmt::datetime_iso "$ts")
250
251 cmd::watch::format_event \
252 "$formatted_ts" "wg" "$client" "${endpoint:-—}" "$event" "blocked"
253
254 else
255 # FW event
256 $allowed_only && continue
257 $blocked_only && continue # fw drops aren't "blocked peers" per se
258
259 local fw_data
260 fw_data=$(json::parse_fw_event "$line")
261 [[ -z "$fw_data" ]] && continue
262
263 local ts src_ip dst_ip dst_port proto
264 IFS="|" read -r ts src_ip dst_ip dst_port proto <<< "$fw_data"
265
266 [[ -z "$src_ip" ]] && continue
267
268 local fw_key="${src_ip}:${dst_ip}:${dst_port}:${proto}"
269 local now
270 now=$(date +%s)
271 local last_fw="${_WATCH_LAST_FW[$fw_key]:-0}"
272
273 local window=30
274 [[ "$proto" == "17" || "$proto" == "udp" ]] && window=10
275 [[ "$proto" == "1" || "$proto" == "icmp" ]] && window=5
276
277 local diff=$(( now - last_fw ))
278 (( diff < window )) && continue
279 _WATCH_LAST_FW["$fw_key"]="$now"
280
281 local client="${ip_to_name[$src_ip]:-$src_ip}"
282
283 [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
284 cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue
285
286 if [[ -n "$filter_type" ]]; then
287 local peer_client="${ip_to_name[$src_ip]:-}"
288 [[ -z "$peer_client" ]] && continue
289 local conf
290 conf="$(ctx::clients)/${peer_client}.conf"
291 [[ -f "$conf" ]] || continue
292 local ip
293 ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
294 local subnet
295 subnet=$(config::subnet_for "$filter_type")
296 string::starts_with "$ip" "$subnet" || continue
297 fi
298
299 local dst_str="${dst_ip:-—}"
300 [[ -n "$dst_port" ]] && dst_str="${dst_ip}:${dst_port}/${proto}"
301
302 local formatted_ts
303 formatted_ts=$(fmt::datetime_iso "$ts")
304
305 cmd::watch::format_event \
306 "$formatted_ts" "fw" "$client" "$dst_str" "drop" "blocked"
307 fi
308 done
309
310 rm -f "$source_file"
311}
312
313# ============================================
314# Run
315# ============================================
316
317function cmd::watch::run() {
318 local filter_name="" filter_type="" filter_peers=""
319 local blocked_only=false allowed_only=false restricted_only=false
320
321 rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true
322
323 while [[ $# -gt 0 ]]; do
324 case "$1" in
325 --name) filter_name="$2"; shift 2 ;;
326 --type) filter_type="$2"; shift 2 ;;
327 --peers) filter_peers="$2"; shift 2 ;;
328 --blocked) blocked_only=true; shift ;;
329 --allowed) allowed_only=true; shift ;;
330 --restricted) restricted_only=true; shift ;;
331 --help) cmd::watch::help; return ;;
332 *)
333 log::error "Unknown flag: $1"
334 cmd::watch::help
335 return 1
336 ;;
337 esac
338 done
339
340 cmd::watch::header
341
342 if ! $blocked_only && ! $restricted_only; then
343 (
344 while true; do
345 cmd::watch::poll_handshakes \
346 "$filter_name" "$filter_type" "$allowed_only" "$filter_peers"
347 sleep 5
348 done
349 ) &
350 local poller_pid=$!
351 fi
352
353 cmd::watch::tail_events \
354 "$filter_name" "$filter_type" \
355 "$blocked_only" "$restricted_only" \
356 "$allowed_only" "$filter_peers" &
357 local tailer_pid=$!
358
359 trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
360 rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM
361
362 wait
363}