gistfile1.txt
· 8.4 KiB · Text
Bruto
#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::watch::on_load() {
flag::register --type
flag::register --name
flag::register --peers
flag::register --blocked
flag::register --restricted
flag::register --allowed
flag::register --raw
}
# ============================================
# Help
# ============================================
function cmd::watch::help() {
cat <<EOF
Usage: wgctl watch [options]
Live monitor of WireGuard activity.
- Handshakes from connected peers (green)
- Connection attempts from blocked peers (red)
- Firewall drops from restricted peers (red)
Options:
--name <name> Filter by client name
--type <type> Filter by device type
--blocked Show only blocked peer attempts
--allowed Show only handshakes
--restricted Show only firewall drop events
--raw Show raw IPs without service annotation
Examples:
wgctl watch
wgctl watch --name phone-nuno
wgctl watch --blocked
wgctl watch --type phone
EOF
}
# ============================================
# Run
# ============================================
function cmd::watch::run() {
local filter_name="" filter_type="" filter_peers=""
local blocked_only=false allowed_only=false restricted_only=false
local raw=false
rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true
while [[ $# -gt 0 ]]; do
case "$1" in
--name) filter_name="$2"; shift 2 ;;
--type) filter_type="$2"; shift 2 ;;
--peers) filter_peers="$2"; shift 2 ;;
--blocked) blocked_only=true; shift ;;
--allowed) allowed_only=true; shift ;;
--restricted) restricted_only=true; shift ;;
--raw) raw=true; shift ;;
--help) cmd::watch::help; return ;;
*)
log::error "Unknown flag: $1"
cmd::watch::help
return 1
;;
esac
done
local net_file=""
$raw || net_file="$(ctx::net)"
log::section "wgctl — Live Monitor (Ctrl+C to stop)"
printf "\n"
# Fixed display widths for watch (dynamic measurement not possible in stream)
local w_client=20 w_dest=18
# Handshake poller (background)
if ! $blocked_only && ! $restricted_only; then
(
while true; do
cmd::watch::_poll_handshakes \
"$filter_name" "$filter_type" "$filter_peers" "$w_client" "$w_dest"
sleep 5
done
) &
local poller_pid=$!
fi
# Event tailer (background)
cmd::watch::_tail_events \
"$filter_name" "$filter_type" "$filter_peers" \
"$blocked_only" "$restricted_only" "$allowed_only" \
"$net_file" "$w_client" "$w_dest" &
local tailer_pid=$!
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; printf '\n'; exit 0" INT TERM
wait
}
# ============================================
# Handshake Poller
# ============================================
function cmd::watch::_poll_handshakes() {
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
local w_client="${4:-20}" w_dest="${5:-30}"
local peer_set=()
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
while IFS= read -r line; do
local public_key ts
public_key=$(echo "$line" | awk '{print $1}')
ts=$(echo "$line" | awk '{print $2}')
[[ -z "$ts" || "$ts" == "0" ]] && continue
# Find client by public key
local client_name=""
for conf in "$(ctx::clients)"/*.conf; do
[[ -f "$conf" ]] || continue
local cname
cname=$(basename "$conf" .conf)
local key
key=$(keys::public "$cname" 2>/dev/null || echo "")
if [[ "$key" == "$public_key" ]]; then
client_name="$cname"
break
fi
done
[[ -z "$client_name" ]] && continue
[[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue
# Dedup — only emit if handshake is new
local safe_key
safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1)
local prev_ts_file="/tmp/wgctl_hs_${safe_key}"
local prev_ts="0"
[[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file")
[[ "$ts" == "$prev_ts" ]] && continue
echo "$ts" > "$prev_ts_file"
local ts_fmt
ts_fmt=$(fmt::datetime_short "$ts")
local endpoint
endpoint=$(monitor::endpoint_for_key "$public_key")
ui::watch::wg_row "$ts_fmt" "$client_name" "${endpoint:-—}" "handshake" \
"$w_client" "$w_dest"
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
}
# ============================================
# Event Tailer
# ============================================
function cmd::watch::_tail_events() {
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
local blocked_only="${4:-false}" restricted_only="${5:-false}" allowed_only="${6:-false}"
local net_file="${7:-}" w_client="${8:-20}" w_dest="${9:-30}"
local peer_set=()
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
# Build ip->name map
declare -A ip_to_name=()
while IFS= read -r conf; do
local cname
cname=$(basename "$conf" .conf)
local ip
ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1)
[[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname"
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
# Load net services if not --raw
declare -A _svc_cache=()
function _resolve_dest() {
local dest_ip="${1:-}" dest_port="${2:-}" proto="${3:-}"
[[ -z "$net_file" || ! -f "$net_file" ]] && echo "" && return
local key="${dest_ip}:${dest_port}:${proto}"
if [[ -z "${_svc_cache[$key]+x}" ]]; then
_svc_cache[$key]=$(net::reverse_lookup "$dest_ip" "$dest_port" "$proto" 2>/dev/null || true)
fi
echo "${_svc_cache[$key]:-}"
}
declare -A _WATCH_LAST_FW=()
declare -A _WATCH_LAST_WG=()
local source_file
source_file=$(mktemp)
echo "wg" > "$source_file"
trap "rm -f '$source_file'" EXIT
tail -f "$(ctx::events_log)" "$(ctx::fw_events_log)" 2>/dev/null \
| while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ "$line" == "==> "* ]]; then
[[ "$line" == *"fw_events"* ]] && echo "fw" > "$source_file" || echo "wg" > "$source_file"
continue
fi
local source
source=$(cat "$source_file")
if [[ "$source" == "fw" ]]; then
$allowed_only && continue
local fw_data
fw_data=$(python3 "$(ctx::json_helper)" parse_fw_event "$line" 2>/dev/null) || continue
[[ -z "$fw_data" ]] && continue
local ts src_ip dest_ip dest_port proto
IFS='|' read -r ts src_ip dest_ip dest_port proto <<< "$fw_data"
[[ -z "$src_ip" ]] && continue
local client="${ip_to_name[$src_ip]:-$src_ip}"
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
# Dedup
local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}"
local now; now=$(date +%s)
local window=30
[[ "$proto" == "17" || "$proto" == "udp" ]] && window=10
[[ "$proto" == "1" || "$proto" == "icmp" ]] && window=5
local last="${_WATCH_LAST_FW[$fw_key]:-0}"
(( now - last < window )) && continue
_WATCH_LAST_FW["$fw_key"]="$now"
local ts_fmt
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
local dest_display
dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto")
ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest"
else
$restricted_only && continue
local ev_data
ev_data=$(python3 "$(ctx::json_helper)" parse_event "$line" 2>/dev/null) || continue
[[ -z "$ev_data" ]] && continue
local ts client endpoint event
IFS='|' read -r ts client endpoint event <<< "$ev_data"
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
$blocked_only && [[ "$event" != "attempt" ]] && continue
$allowed_only && [[ "$event" != "handshake" ]] && continue
# Dedup
local wg_key="${client}:${endpoint}:${event}"
local now; now=$(date +%s)
local last="${_WATCH_LAST_WG[$wg_key]:-0}"
(( now - last < 30 )) && continue
_WATCH_LAST_WG["$wg_key"]="$now"
local ts_fmt
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
ui::watch::wg_row "$ts_fmt" "$client" "${endpoint:-—}" "$event" \
"$w_client" "$w_dest"
fi
done
rm -f "$source_file"
}
| 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 | flag::register --raw |
| 15 | } |
| 16 | |
| 17 | # ============================================ |
| 18 | # Help |
| 19 | # ============================================ |
| 20 | |
| 21 | function cmd::watch::help() { |
| 22 | cat <<EOF |
| 23 | Usage: wgctl watch [options] |
| 24 | |
| 25 | Live monitor of WireGuard activity. |
| 26 | - Handshakes from connected peers (green) |
| 27 | - Connection attempts from blocked peers (red) |
| 28 | - Firewall drops from restricted peers (red) |
| 29 | |
| 30 | Options: |
| 31 | --name <name> Filter by client name |
| 32 | --type <type> Filter by device type |
| 33 | --blocked Show only blocked peer attempts |
| 34 | --allowed Show only handshakes |
| 35 | --restricted Show only firewall drop events |
| 36 | --raw Show raw IPs without service annotation |
| 37 | |
| 38 | Examples: |
| 39 | wgctl watch |
| 40 | wgctl watch --name phone-nuno |
| 41 | wgctl watch --blocked |
| 42 | wgctl watch --type phone |
| 43 | EOF |
| 44 | } |
| 45 | |
| 46 | # ============================================ |
| 47 | # Run |
| 48 | # ============================================ |
| 49 | |
| 50 | function cmd::watch::run() { |
| 51 | local filter_name="" filter_type="" filter_peers="" |
| 52 | local blocked_only=false allowed_only=false restricted_only=false |
| 53 | local raw=false |
| 54 | |
| 55 | rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true |
| 56 | |
| 57 | while [[ $# -gt 0 ]]; do |
| 58 | case "$1" in |
| 59 | --name) filter_name="$2"; shift 2 ;; |
| 60 | --type) filter_type="$2"; shift 2 ;; |
| 61 | --peers) filter_peers="$2"; shift 2 ;; |
| 62 | --blocked) blocked_only=true; shift ;; |
| 63 | --allowed) allowed_only=true; shift ;; |
| 64 | --restricted) restricted_only=true; shift ;; |
| 65 | --raw) raw=true; shift ;; |
| 66 | --help) cmd::watch::help; return ;; |
| 67 | *) |
| 68 | log::error "Unknown flag: $1" |
| 69 | cmd::watch::help |
| 70 | return 1 |
| 71 | ;; |
| 72 | esac |
| 73 | done |
| 74 | |
| 75 | local net_file="" |
| 76 | $raw || net_file="$(ctx::net)" |
| 77 | |
| 78 | log::section "wgctl — Live Monitor (Ctrl+C to stop)" |
| 79 | printf "\n" |
| 80 | |
| 81 | # Fixed display widths for watch (dynamic measurement not possible in stream) |
| 82 | local w_client=20 w_dest=18 |
| 83 | |
| 84 | # Handshake poller (background) |
| 85 | if ! $blocked_only && ! $restricted_only; then |
| 86 | ( |
| 87 | while true; do |
| 88 | cmd::watch::_poll_handshakes \ |
| 89 | "$filter_name" "$filter_type" "$filter_peers" "$w_client" "$w_dest" |
| 90 | sleep 5 |
| 91 | done |
| 92 | ) & |
| 93 | local poller_pid=$! |
| 94 | fi |
| 95 | |
| 96 | # Event tailer (background) |
| 97 | cmd::watch::_tail_events \ |
| 98 | "$filter_name" "$filter_type" "$filter_peers" \ |
| 99 | "$blocked_only" "$restricted_only" "$allowed_only" \ |
| 100 | "$net_file" "$w_client" "$w_dest" & |
| 101 | local tailer_pid=$! |
| 102 | |
| 103 | trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \ |
| 104 | rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; printf '\n'; exit 0" INT TERM |
| 105 | |
| 106 | wait |
| 107 | } |
| 108 | |
| 109 | # ============================================ |
| 110 | # Handshake Poller |
| 111 | # ============================================ |
| 112 | |
| 113 | function cmd::watch::_poll_handshakes() { |
| 114 | local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" |
| 115 | local w_client="${4:-20}" w_dest="${5:-30}" |
| 116 | |
| 117 | local peer_set=() |
| 118 | [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers" |
| 119 | |
| 120 | while IFS= read -r line; do |
| 121 | local public_key ts |
| 122 | public_key=$(echo "$line" | awk '{print $1}') |
| 123 | ts=$(echo "$line" | awk '{print $2}') |
| 124 | [[ -z "$ts" || "$ts" == "0" ]] && continue |
| 125 | |
| 126 | # Find client by public key |
| 127 | local client_name="" |
| 128 | for conf in "$(ctx::clients)"/*.conf; do |
| 129 | [[ -f "$conf" ]] || continue |
| 130 | local cname |
| 131 | cname=$(basename "$conf" .conf) |
| 132 | local key |
| 133 | key=$(keys::public "$cname" 2>/dev/null || echo "") |
| 134 | if [[ "$key" == "$public_key" ]]; then |
| 135 | client_name="$cname" |
| 136 | break |
| 137 | fi |
| 138 | done |
| 139 | [[ -z "$client_name" ]] && continue |
| 140 | [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue |
| 141 | |
| 142 | # Dedup — only emit if handshake is new |
| 143 | local safe_key |
| 144 | safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1) |
| 145 | local prev_ts_file="/tmp/wgctl_hs_${safe_key}" |
| 146 | local prev_ts="0" |
| 147 | [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file") |
| 148 | [[ "$ts" == "$prev_ts" ]] && continue |
| 149 | echo "$ts" > "$prev_ts_file" |
| 150 | |
| 151 | local ts_fmt |
| 152 | ts_fmt=$(fmt::datetime_short "$ts") |
| 153 | local endpoint |
| 154 | endpoint=$(monitor::endpoint_for_key "$public_key") |
| 155 | |
| 156 | ui::watch::wg_row "$ts_fmt" "$client_name" "${endpoint:-—}" "handshake" \ |
| 157 | "$w_client" "$w_dest" |
| 158 | |
| 159 | done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) |
| 160 | } |
| 161 | |
| 162 | # ============================================ |
| 163 | # Event Tailer |
| 164 | # ============================================ |
| 165 | |
| 166 | function cmd::watch::_tail_events() { |
| 167 | local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" |
| 168 | local blocked_only="${4:-false}" restricted_only="${5:-false}" allowed_only="${6:-false}" |
| 169 | local net_file="${7:-}" w_client="${8:-20}" w_dest="${9:-30}" |
| 170 | |
| 171 | local peer_set=() |
| 172 | [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers" |
| 173 | |
| 174 | # Build ip->name map |
| 175 | declare -A ip_to_name=() |
| 176 | while IFS= read -r conf; do |
| 177 | local cname |
| 178 | cname=$(basename "$conf" .conf) |
| 179 | local ip |
| 180 | ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1) |
| 181 | [[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname" |
| 182 | done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null) |
| 183 | |
| 184 | # Load net services if not --raw |
| 185 | declare -A _svc_cache=() |
| 186 | function _resolve_dest() { |
| 187 | local dest_ip="${1:-}" dest_port="${2:-}" proto="${3:-}" |
| 188 | [[ -z "$net_file" || ! -f "$net_file" ]] && echo "" && return |
| 189 | local key="${dest_ip}:${dest_port}:${proto}" |
| 190 | if [[ -z "${_svc_cache[$key]+x}" ]]; then |
| 191 | _svc_cache[$key]=$(net::reverse_lookup "$dest_ip" "$dest_port" "$proto" 2>/dev/null || true) |
| 192 | fi |
| 193 | echo "${_svc_cache[$key]:-}" |
| 194 | } |
| 195 | |
| 196 | declare -A _WATCH_LAST_FW=() |
| 197 | declare -A _WATCH_LAST_WG=() |
| 198 | |
| 199 | local source_file |
| 200 | source_file=$(mktemp) |
| 201 | echo "wg" > "$source_file" |
| 202 | trap "rm -f '$source_file'" EXIT |
| 203 | |
| 204 | tail -f "$(ctx::events_log)" "$(ctx::fw_events_log)" 2>/dev/null \ |
| 205 | | while IFS= read -r line; do |
| 206 | [[ -z "$line" ]] && continue |
| 207 | |
| 208 | if [[ "$line" == "==> "* ]]; then |
| 209 | [[ "$line" == *"fw_events"* ]] && echo "fw" > "$source_file" || echo "wg" > "$source_file" |
| 210 | continue |
| 211 | fi |
| 212 | |
| 213 | local source |
| 214 | source=$(cat "$source_file") |
| 215 | |
| 216 | if [[ "$source" == "fw" ]]; then |
| 217 | $allowed_only && continue |
| 218 | |
| 219 | local fw_data |
| 220 | fw_data=$(python3 "$(ctx::json_helper)" parse_fw_event "$line" 2>/dev/null) || continue |
| 221 | [[ -z "$fw_data" ]] && continue |
| 222 | |
| 223 | local ts src_ip dest_ip dest_port proto |
| 224 | IFS='|' read -r ts src_ip dest_ip dest_port proto <<< "$fw_data" |
| 225 | [[ -z "$src_ip" ]] && continue |
| 226 | |
| 227 | local client="${ip_to_name[$src_ip]:-$src_ip}" |
| 228 | [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue |
| 229 | |
| 230 | # Dedup |
| 231 | local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}" |
| 232 | local now; now=$(date +%s) |
| 233 | local window=30 |
| 234 | [[ "$proto" == "17" || "$proto" == "udp" ]] && window=10 |
| 235 | [[ "$proto" == "1" || "$proto" == "icmp" ]] && window=5 |
| 236 | local last="${_WATCH_LAST_FW[$fw_key]:-0}" |
| 237 | (( now - last < window )) && continue |
| 238 | _WATCH_LAST_FW["$fw_key"]="$now" |
| 239 | |
| 240 | local ts_fmt |
| 241 | ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") |
| 242 | |
| 243 | local dest_display |
| 244 | dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto") |
| 245 | |
| 246 | ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest" |
| 247 | |
| 248 | else |
| 249 | $restricted_only && continue |
| 250 | |
| 251 | local ev_data |
| 252 | ev_data=$(python3 "$(ctx::json_helper)" parse_event "$line" 2>/dev/null) || continue |
| 253 | [[ -z "$ev_data" ]] && continue |
| 254 | |
| 255 | local ts client endpoint event |
| 256 | IFS='|' read -r ts client endpoint event <<< "$ev_data" |
| 257 | [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue |
| 258 | $blocked_only && [[ "$event" != "attempt" ]] && continue |
| 259 | $allowed_only && [[ "$event" != "handshake" ]] && continue |
| 260 | |
| 261 | # Dedup |
| 262 | local wg_key="${client}:${endpoint}:${event}" |
| 263 | local now; now=$(date +%s) |
| 264 | local last="${_WATCH_LAST_WG[$wg_key]:-0}" |
| 265 | (( now - last < 30 )) && continue |
| 266 | _WATCH_LAST_WG["$wg_key"]="$now" |
| 267 | |
| 268 | local ts_fmt |
| 269 | ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") |
| 270 | |
| 271 | ui::watch::wg_row "$ts_fmt" "$client" "${endpoint:-—}" "$event" \ |
| 272 | "$w_client" "$w_dest" |
| 273 | fi |
| 274 | done |
| 275 | |
| 276 | rm -f "$source_file" |
| 277 | } |