gistfile1.txt
· 7.3 KiB · Text
Originalformat
#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::watch::on_load() {
flag::register --type
flag::register --name
flag::register --blocked
flag::register --restricted
flag::register --allowed
}
# ============================================
# Help
# ============================================
function cmd::watch::help() {
cat <<EOF
Usage: wgctl watch [options]
Live monitor of WireGuard activity.
Shows handshakes from active clients and connection attempts from blocked clients.
Options:
--type <type> Filter by device type
--name <name> Filter by client name
--allowed Show only allowed client handshakes
--restricted Show only restricted client events
--blocked Show only blocked client attempts
Examples:
wgctl watch
wgctl watch --blocked
wgctl watch --allowed
wgctl watch --type phone
wgctl watch --name phone-nuno
EOF
}
# ============================================
# Helpers
# ============================================
function cmd::watch::format_event() {
local ts="$1"
local client="$2"
local endpoint="$3"
local event="$4"
local status="$5"
local event_color
case "$event" in
attempt) event_color="\033[1;31m" ;; # red
handshake) event_color="\033[1;32m" ;; # green
*) event_color="\033[0;37m" ;; # grey
esac
local status_str=""
if [[ -n "$status" ]]; then
case "$status" in
blocked) status_str=" \033[1;31mblocked\033[0m" ;;
allowed) status_str=" \033[1;32mallowed\033[0m" ;;
*) status_str="" ;;
esac
fi
printf " %-20s %-25s %-20s ${event_color}%-12s\033[0m%b\n" \
"$ts" "$client" "${endpoint:-—}" "$event" "$status_str"
}
function cmd::watch::header() {
log::section "wgctl — Live Monitor (Ctrl+C to stop)"
printf "\n %-20s %-25s %-20s %-12s %s\n" \
"TIME" "CLIENT" "ENDPOINT" "EVENT" "STATUS"
printf " %s\n\n" "$(printf '─%.0s' {1..85})"
}
# ============================================
# Handshake Poller
# ============================================
declare -A _WATCH_LAST_HANDSHAKES=()
function cmd::watch::poll_handshakes() {
local filter_name="$1"
local filter_type="$2"
local allowed_only="$3"
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 name
name=$(basename "$conf" .conf)
local key
key=$(keys::public "$name" 2>/dev/null || echo "")
if [[ "$key" == "$public_key" ]]; then
client_name="$name"
break
fi
done
[[ -z "$client_name" ]] && continue
# Apply filters
[[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue
if [[ -n "$filter_type" ]]; then
local ip
ip=$(grep "^Address" "$(ctx::clients)/${client_name}.conf" | awk '{print $3}' | cut -d'/' -f1)
local subnet
subnet=$(config::subnet_for "$filter_type")
string::starts_with "$ip" "$subnet" || continue
fi
# 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")
if [[ "$ts" != "$prev_ts" ]]; then
echo "$ts" > "$prev_ts_file"
local formatted_ts
formatted_ts=$(fmt::datetime "$ts")
local endpoint
endpoint=$(monitor::endpoint_for_key "$public_key")
cmd::watch::format_event \
"$formatted_ts" "$client_name" "${endpoint:-—}" "handshake" "allowed"
fi
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
}
# ============================================
# Event Tailer
# ============================================
function cmd::watch::tail_events() {
local filter_name="$1"
local filter_type="$2"
local blocked_only="$3"
local restricted_only="$4"
local allowed_only="$5"
declare -A _WATCH_LAST_ATTEMPT=()
tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do
[[ -z "$line" ]] && continue
local event_data
event_data=$(json::parse_event "$line")
[[ -z "$event_data" ]] && continue
local ts client endpoint event
IFS="|" read -r ts client endpoint event <<< "$event_data"
if $restricted_only; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
cmd::list::is_restricted "$client" || continue
fi
# Apply filters
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
if [[ -n "$filter_type" ]]; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local subnet
subnet=$(config::subnet_for "$filter_type")
string::starts_with "$ip" "$subnet" || continue
fi
# Filter by status
if $allowed_only && [[ "$event" != "handshake" ]]; then
continue
fi
if $restricted_only; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
cmd::list::is_restricted "$client" || continue
fi
local formatted_ts
formatted_ts=$(fmt::datetime_iso "$ts")
# Before printing the event
local now
now=$(date +%s)
local safe_client="${client//[-.]/_}"
local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}"
local diff=$(( now - last ))
if (( diff < 30 )); then
continue
fi
_WATCH_LAST_ATTEMPT[$safe_client]="$now"
cmd::watch::format_event \
"$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked"
done
}
# ============================================
# Run
# ============================================
function cmd::watch::run() {
local filter_name=""
local filter_type=""
local blocked_only=false
local allowed_only=false
local restricted_only=false
# Clean up any stale temp files from previous runs
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 ;;
--blocked) blocked_only=true; shift ;;
--allowed) allowed_only=true; shift ;;
--restricted) restricted_only=true; shift ;;
--help) cmd::watch::help; return ;;
*)
log::error "Unknown flag: $1"
cmd::watch::help
return 1
;;
esac
done
cmd::watch::header
# Start event tailer in background unless --blocked not set
if ! $blocked_only && ! $restricted_only; then
# Poll handshakes every 5 seconds in background
(
while true; do
cmd::watch::poll_handshakes "$filter_name" "$filter_type" "$allowed_only"
sleep 5
done
) &
local poller_pid=$!
fi
# Tail events log for blocked attempts
cmd::watch::tail_events "$filter_name" "$filter_type" "$blocked_only" "$restricted_only" "$allowed_only" &
local tailer_pid=$!
# Trap Ctrl+C to clean up background processes
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM
# Keep main process alive
wait
}
| 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 --blocked |
| 11 | flag::register --restricted |
| 12 | flag::register --allowed |
| 13 | } |
| 14 | |
| 15 | # ============================================ |
| 16 | # Help |
| 17 | # ============================================ |
| 18 | |
| 19 | function cmd::watch::help() { |
| 20 | cat <<EOF |
| 21 | Usage: wgctl watch [options] |
| 22 | |
| 23 | Live monitor of WireGuard activity. |
| 24 | Shows handshakes from active clients and connection attempts from blocked clients. |
| 25 | |
| 26 | Options: |
| 27 | --type <type> Filter by device type |
| 28 | --name <name> Filter by client name |
| 29 | --allowed Show only allowed client handshakes |
| 30 | --restricted Show only restricted client events |
| 31 | --blocked Show only blocked client attempts |
| 32 | |
| 33 | Examples: |
| 34 | wgctl watch |
| 35 | wgctl watch --blocked |
| 36 | wgctl watch --allowed |
| 37 | wgctl watch --type phone |
| 38 | wgctl watch --name phone-nuno |
| 39 | EOF |
| 40 | } |
| 41 | |
| 42 | # ============================================ |
| 43 | # Helpers |
| 44 | # ============================================ |
| 45 | |
| 46 | function cmd::watch::format_event() { |
| 47 | local ts="$1" |
| 48 | local client="$2" |
| 49 | local endpoint="$3" |
| 50 | local event="$4" |
| 51 | local status="$5" |
| 52 | |
| 53 | local event_color |
| 54 | case "$event" in |
| 55 | attempt) event_color="\033[1;31m" ;; # red |
| 56 | handshake) event_color="\033[1;32m" ;; # green |
| 57 | *) event_color="\033[0;37m" ;; # grey |
| 58 | esac |
| 59 | |
| 60 | local status_str="" |
| 61 | if [[ -n "$status" ]]; then |
| 62 | case "$status" in |
| 63 | blocked) status_str=" \033[1;31mblocked\033[0m" ;; |
| 64 | allowed) status_str=" \033[1;32mallowed\033[0m" ;; |
| 65 | *) status_str="" ;; |
| 66 | esac |
| 67 | fi |
| 68 | |
| 69 | printf " %-20s %-25s %-20s ${event_color}%-12s\033[0m%b\n" \ |
| 70 | "$ts" "$client" "${endpoint:-—}" "$event" "$status_str" |
| 71 | } |
| 72 | |
| 73 | function cmd::watch::header() { |
| 74 | log::section "wgctl — Live Monitor (Ctrl+C to stop)" |
| 75 | printf "\n %-20s %-25s %-20s %-12s %s\n" \ |
| 76 | "TIME" "CLIENT" "ENDPOINT" "EVENT" "STATUS" |
| 77 | printf " %s\n\n" "$(printf '─%.0s' {1..85})" |
| 78 | } |
| 79 | |
| 80 | # ============================================ |
| 81 | # Handshake Poller |
| 82 | # ============================================ |
| 83 | |
| 84 | declare -A _WATCH_LAST_HANDSHAKES=() |
| 85 | |
| 86 | function cmd::watch::poll_handshakes() { |
| 87 | local filter_name="$1" |
| 88 | local filter_type="$2" |
| 89 | local allowed_only="$3" |
| 90 | |
| 91 | while IFS= read -r line; do |
| 92 | local public_key ts |
| 93 | public_key=$(echo "$line" | awk '{print $1}') |
| 94 | ts=$(echo "$line" | awk '{print $2}') |
| 95 | |
| 96 | [[ -z "$ts" || "$ts" == "0" ]] && continue |
| 97 | |
| 98 | # Find client by public key |
| 99 | local client_name="" |
| 100 | for conf in "$(ctx::clients)"/*.conf; do |
| 101 | [[ -f "$conf" ]] || continue |
| 102 | local name |
| 103 | name=$(basename "$conf" .conf) |
| 104 | local key |
| 105 | key=$(keys::public "$name" 2>/dev/null || echo "") |
| 106 | if [[ "$key" == "$public_key" ]]; then |
| 107 | client_name="$name" |
| 108 | break |
| 109 | fi |
| 110 | done |
| 111 | |
| 112 | [[ -z "$client_name" ]] && continue |
| 113 | |
| 114 | # Apply filters |
| 115 | [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue |
| 116 | |
| 117 | if [[ -n "$filter_type" ]]; then |
| 118 | local ip |
| 119 | ip=$(grep "^Address" "$(ctx::clients)/${client_name}.conf" | awk '{print $3}' | cut -d'/' -f1) |
| 120 | local subnet |
| 121 | subnet=$(config::subnet_for "$filter_type") |
| 122 | string::starts_with "$ip" "$subnet" || continue |
| 123 | fi |
| 124 | |
| 125 | # Only emit if handshake is new |
| 126 | local safe_key |
| 127 | safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1) |
| 128 | local prev_ts_file="/tmp/wgctl_hs_${safe_key}" |
| 129 | local prev_ts="0" |
| 130 | [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file") |
| 131 | if [[ "$ts" != "$prev_ts" ]]; then |
| 132 | echo "$ts" > "$prev_ts_file" |
| 133 | |
| 134 | local formatted_ts |
| 135 | formatted_ts=$(fmt::datetime "$ts") |
| 136 | |
| 137 | local endpoint |
| 138 | endpoint=$(monitor::endpoint_for_key "$public_key") |
| 139 | |
| 140 | cmd::watch::format_event \ |
| 141 | "$formatted_ts" "$client_name" "${endpoint:-—}" "handshake" "allowed" |
| 142 | fi |
| 143 | |
| 144 | done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) |
| 145 | } |
| 146 | |
| 147 | # ============================================ |
| 148 | # Event Tailer |
| 149 | # ============================================ |
| 150 | |
| 151 | function cmd::watch::tail_events() { |
| 152 | local filter_name="$1" |
| 153 | local filter_type="$2" |
| 154 | local blocked_only="$3" |
| 155 | local restricted_only="$4" |
| 156 | local allowed_only="$5" |
| 157 | |
| 158 | declare -A _WATCH_LAST_ATTEMPT=() |
| 159 | |
| 160 | tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do |
| 161 | [[ -z "$line" ]] && continue |
| 162 | |
| 163 | local event_data |
| 164 | event_data=$(json::parse_event "$line") |
| 165 | |
| 166 | [[ -z "$event_data" ]] && continue |
| 167 | |
| 168 | local ts client endpoint event |
| 169 | IFS="|" read -r ts client endpoint event <<< "$event_data" |
| 170 | |
| 171 | if $restricted_only; then |
| 172 | local conf |
| 173 | conf="$(ctx::clients)/${client}.conf" |
| 174 | [[ -f "$conf" ]] || continue |
| 175 | cmd::list::is_restricted "$client" || continue |
| 176 | fi |
| 177 | |
| 178 | # Apply filters |
| 179 | [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue |
| 180 | |
| 181 | if [[ -n "$filter_type" ]]; then |
| 182 | local conf |
| 183 | conf="$(ctx::clients)/${client}.conf" |
| 184 | [[ -f "$conf" ]] || continue |
| 185 | local ip |
| 186 | ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 187 | local subnet |
| 188 | subnet=$(config::subnet_for "$filter_type") |
| 189 | string::starts_with "$ip" "$subnet" || continue |
| 190 | fi |
| 191 | |
| 192 | # Filter by status |
| 193 | if $allowed_only && [[ "$event" != "handshake" ]]; then |
| 194 | continue |
| 195 | fi |
| 196 | |
| 197 | if $restricted_only; then |
| 198 | local conf |
| 199 | conf="$(ctx::clients)/${client}.conf" |
| 200 | [[ -f "$conf" ]] || continue |
| 201 | cmd::list::is_restricted "$client" || continue |
| 202 | fi |
| 203 | |
| 204 | local formatted_ts |
| 205 | formatted_ts=$(fmt::datetime_iso "$ts") |
| 206 | |
| 207 | # Before printing the event |
| 208 | local now |
| 209 | now=$(date +%s) |
| 210 | local safe_client="${client//[-.]/_}" |
| 211 | local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}" |
| 212 | local diff=$(( now - last )) |
| 213 | if (( diff < 30 )); then |
| 214 | continue |
| 215 | fi |
| 216 | _WATCH_LAST_ATTEMPT[$safe_client]="$now" |
| 217 | |
| 218 | cmd::watch::format_event \ |
| 219 | "$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked" |
| 220 | done |
| 221 | } |
| 222 | |
| 223 | # ============================================ |
| 224 | # Run |
| 225 | # ============================================ |
| 226 | |
| 227 | function cmd::watch::run() { |
| 228 | local filter_name="" |
| 229 | local filter_type="" |
| 230 | local blocked_only=false |
| 231 | local allowed_only=false |
| 232 | local restricted_only=false |
| 233 | |
| 234 | # Clean up any stale temp files from previous runs |
| 235 | rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true |
| 236 | |
| 237 | while [[ $# -gt 0 ]]; do |
| 238 | case "$1" in |
| 239 | --name) filter_name="$2"; shift 2 ;; |
| 240 | --type) filter_type="$2"; shift 2 ;; |
| 241 | --blocked) blocked_only=true; shift ;; |
| 242 | --allowed) allowed_only=true; shift ;; |
| 243 | --restricted) restricted_only=true; shift ;; |
| 244 | --help) cmd::watch::help; return ;; |
| 245 | *) |
| 246 | log::error "Unknown flag: $1" |
| 247 | cmd::watch::help |
| 248 | return 1 |
| 249 | ;; |
| 250 | esac |
| 251 | done |
| 252 | |
| 253 | cmd::watch::header |
| 254 | |
| 255 | # Start event tailer in background unless --blocked not set |
| 256 | if ! $blocked_only && ! $restricted_only; then |
| 257 | # Poll handshakes every 5 seconds in background |
| 258 | ( |
| 259 | while true; do |
| 260 | cmd::watch::poll_handshakes "$filter_name" "$filter_type" "$allowed_only" |
| 261 | sleep 5 |
| 262 | done |
| 263 | ) & |
| 264 | local poller_pid=$! |
| 265 | fi |
| 266 | |
| 267 | # Tail events log for blocked attempts |
| 268 | cmd::watch::tail_events "$filter_name" "$filter_type" "$blocked_only" "$restricted_only" "$allowed_only" & |
| 269 | local tailer_pid=$! |
| 270 | |
| 271 | # Trap Ctrl+C to clean up background processes |
| 272 | trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM |
| 273 | |
| 274 | # Keep main process alive |
| 275 | wait |
| 276 | } |
| 277 |