Última actividad 1 month ago

nuno revisó este gist 1 month ago. Ir a la revisión

1 file changed, 363 insertions

gistfile1.txt(archivo creado)

@@ -0,0 +1,363 @@
1 + #!/usr/bin/env bash
2 +
3 + # ============================================
4 + # Lifecycle
5 + # ============================================
6 +
7 + function 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 +
20 + function cmd::watch::help() {
21 + cat <<EOF
22 + Usage: wgctl watch [options]
23 +
24 + Live 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 +
29 + Options:
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 +
37 + Examples:
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
44 + EOF
45 + }
46 +
47 + # ============================================
48 + # Helpers
49 + # ============================================
50 +
51 + function 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 +
72 + function 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 +
79 + function 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 +
94 + function 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 +
165 + function 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 +
317 + function 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 + }
Siguiente Anterior