#!/usr/bin/env bash # ============================================ # Config # ============================================ MONITOR_DIR="$(ctx::root)/daemon" WATCHLIST_FILE="${MONITOR_DIR}/watchlist.json" EVENTS_LOG="${MONITOR_DIR}/events.log" MONITOR_SERVICE="wgctl-monitor" # ============================================ # Lifecycle # ============================================ function monitor::on_load() { if [[ ! -f "$WATCHLIST_FILE" ]]; then echo '{}' > "$WATCHLIST_FILE" fi if [[ ! -f "$EVENTS_LOG" ]]; then touch "$EVENTS_LOG" fi } # ============================================ # Watchlist # ============================================ function monitor::watch() { local ip="$1" local client="$2" python3 -c " import json with open('${WATCHLIST_FILE}') as f: wl = json.load(f) wl['${ip}'] = '${client}' with open('${WATCHLIST_FILE}', 'w') as f: json.dump(wl, f, indent=2) " log::wg "Watching: ${client} (${ip})" } function monitor::unwatch() { local ip="$1" python3 -c " import json with open('${WATCHLIST_FILE}') as f: wl = json.load(f) wl.pop('${ip}', None) with open('${WATCHLIST_FILE}', 'w') as f: json.dump(wl, f, indent=2) " log::wg "Unwatched: ${ip}" } function monitor::is_watched() { local ip="$1" python3 -c " import json with open('${WATCHLIST_FILE}') as f: wl = json.load(f) exit(0 if '${ip}' in wl else 1) " } function monitor::unwatch_client() { local name="$1" python3 -c " import json with open('${WATCHLIST_FILE}') as f: wl = json.load(f) wl = {k: v for k, v in wl.items() if v != '${name}'} with open('${WATCHLIST_FILE}', 'w') as f: json.dump(wl, f, indent=2) " log::wg "Unwatched client: ${name}" } # ============================================ # Events # ============================================ function monitor::last_attempt() { local client="$1" python3 -c " import json events = [] try: with open('${EVENTS_LOG}') as f: for line in f: try: e = json.loads(line.strip()) if e.get('client') == '${client}': events.append(e) except: pass except: pass if events: print(events[-1].get('timestamp', '')) " } function monitor::last_endpoint() { local client="$1" python3 -c " import json events = [] try: with open('${EVENTS_LOG}') as f: for line in f: try: e = json.loads(line.strip()) if e.get('client') == '${client}' and e.get('endpoint'): events.append(e) except: pass except: pass if events: print(events[-1].get('endpoint', '')) " } function monitor::events_for() { local ip="$1" local limit="${2:-50}" python3 -c " import json from datetime import datetime, timezone events = [] try: with open('${EVENTS_LOG}') as f: for line in f: try: e = json.loads(line.strip()) if e.get('ip') == '${ip}': events.append(e) except: pass except: pass for e in events[-${limit}:]: ts = e.get('timestamp', '') try: dt = datetime.fromisoformat(ts) ts = dt.strftime('%Y-%m-%d %H:%M:%S') except: pass endpoint = e.get('endpoint', '—') client = e.get('client', '—') event = e.get('event', '—') print(f' {ts} {client:<20} {endpoint:<20} {event}') " } # ============================================ # Endpoint Cache (for blocked clients) # ============================================ ENDPOINT_CACHE="${WGCTL_DIR}/daemon/endpoint_cache.json" function monitor::cache_endpoint() { local client="$1" local endpoint="$2" [[ -z "$endpoint" || "$endpoint" == "(none)" ]] && return 0 python3 -c " import json cache = {} try: with open('${ENDPOINT_CACHE}') as f: cache = json.load(f) except: pass cache['${client}'] = '${endpoint}' with open('${ENDPOINT_CACHE}', 'w') as f: json.dump(cache, f, indent=2) " } function monitor::get_cached_endpoint() { local client="$1" python3 -c " import json try: with open('${ENDPOINT_CACHE}') as f: cache = json.load(f) print(cache.get('${client}', '')) except: print('') " } function monitor::update_endpoint_cache() { while IFS=$'\t' read -r key endpoint; do [[ "$endpoint" == "(none)" ]] && continue local ip ip=$(echo "$endpoint" | cut -d':' -f1) local client client=$(keys::find_by_public "$key") || continue monitor::cache_endpoint "$client" "$ip" done < <(wg show "$(config::interface)" endpoints 2>/dev/null) } # ============================================ # Endpoint (from wg show, for active clients) # ============================================ function monitor::endpoint_for_key() { local public_key="$1" wg show "$(config::interface)" endpoints 2>/dev/null \ | grep "^${public_key}" \ | awk '{print $2}' \ | cut -d':' -f1 } # ============================================ # Service # ============================================ function monitor::start() { systemctl start "$MONITOR_SERVICE" log::wg_start "Monitor daemon started" } function monitor::stop() { systemctl stop "$MONITOR_SERVICE" log::wg_stop "Monitor daemon stopped" } function monitor::restart() { systemctl restart "$MONITOR_SERVICE" log::wg_start "Monitor daemon restarted" } function monitor::is_running() { systemctl is-active --quiet "$MONITOR_SERVICE" }