diff --git a/commands/logs.command.sh b/commands/logs.command.sh index cd9c155..b5b24e9 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -11,10 +11,12 @@ function cmd::logs::on_load() { flag::register --fw flag::register --wg flag::register --follow + flag::register --merged flag::register --all flag::register --before flag::register --force flag::register --days + flag::register --raw } function cmd::logs::help() { @@ -34,7 +36,9 @@ Options for show: --limit Max results per source (default: 50) --fw Show only firewall drops --wg Show only WireGuard events - --follow, -f Follow logs in real time + --merged Show all events chronologically interleaved + --follow, -f Follow logs in real time (alias: wgctl watch) + --raw Show raw IPs without service annotation Options for remove: --name Remove entries for specific peer @@ -52,12 +56,10 @@ Examples: wgctl logs wgctl logs --name phone-nuno wgctl logs --fw --limit 100 + wgctl logs --merged wgctl logs --follow wgctl logs remove --name phone-nuno - wgctl logs remove --all --force - wgctl logs remove --fw --before 1 - wgctl logs rotate - wgctl logs rotate --days 30 --force + wgctl logs rotate --days 30 EOF } @@ -70,10 +72,10 @@ function cmd::logs::run() { fi case "$subcmd" in - show) cmd::logs::show "$@" ;; - remove|rm|del) cmd::logs::remove "$@" ;; - rotate) cmd::logs::rotate "$@" ;; - help) cmd::logs::help ;; + show) cmd::logs::show "$@" ;; + remove|rm|del) cmd::logs::remove "$@" ;; + rotate) cmd::logs::rotate "$@" ;; + help) cmd::logs::help ;; *) log::error "Unknown subcommand: '${subcmd}'" cmd::logs::help @@ -84,17 +86,19 @@ function cmd::logs::run() { function cmd::logs::show() { local name="" type="" limit=50 - local fw_only=false wg_only=false follow=false + local fw_only=false wg_only=false follow=false merged=false raw=false while [[ $# -gt 0 ]]; do case "$1" in - --name) name="$2"; shift 2 ;; - --type) type="$2"; shift 2 ;; - --limit) limit="$2"; shift 2 ;; - --fw) fw_only=true; shift ;; - --wg) wg_only=true; shift ;; - --follow|-f) follow=true; shift ;; - --help) cmd::logs::help; return ;; + --name) name="$2"; shift 2 ;; + --type) type="$2"; shift 2 ;; + --limit) limit="$2"; shift 2 ;; + --fw) fw_only=true; shift ;; + --wg) wg_only=true; shift ;; + --merged) merged=true; shift ;; + --follow|-f) follow=true; shift ;; + --raw) raw=true; shift ;; + --help) cmd::logs::help; return ;; *) log::error "Unknown flag: $1" return 1 @@ -117,51 +121,171 @@ function cmd::logs::show() { return fi + local net_file="" + $raw || net_file="$(ctx::net)" + log::section "WireGuard Activity Log" printf "\n" - $wg_only || cmd::logs::show_fw_events "$filter_ip" "$name" "$type" "$limit" - $fw_only || cmd::logs::show_wg_events "$filter_ip" "$name" "$type" "$limit" + + if $merged; then + cmd::logs::show_merged "$filter_ip" "$name" "$type" "$limit" "$net_file" + return + fi + + $wg_only || cmd::logs::show_fw_events "$filter_ip" "$name" "$type" "$limit" "$net_file" + $fw_only || cmd::logs::show_wg_events "$filter_ip" "$name" "$type" "$limit" +} + +function cmd::logs::show_fw_events() { + local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \ + limit="${4:-50}" net_file="${5:-}" + + [[ ! -f "$FW_EVENTS_LOG" ]] && return 0 + + local data + data=$(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \ + "$(ctx::clients)" "${net_file:-}" "$limit" 2>/dev/null) + + [[ -z "$data" ]] && return 0 + + # Measure column widths + local w_client=16 w_dest=20 + while IFS='|' read -r ts client dest_ip dest_port proto svc count; do + [[ -z "$ts" ]] && continue + (( ${#client} > w_client )) && w_client=${#client} + local dest_display + if [[ -n "$svc" ]]; then + [[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})" + else + [[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})" + fi + (( ${#dest_display} > w_dest )) && w_dest=${#dest_display} + done <<< "$data" + (( w_client += 2 )) + (( w_dest += 2 )) + + ui::logs::fw_section_header + while IFS='|' read -r ts client dest_ip dest_port proto svc count; do + [[ -z "$ts" ]] && continue + ui::logs::fw_row "$ts" "$client" "$dest_ip" "$dest_port" \ + "$proto" "$svc" "$count" "$w_client" "$w_dest" + done <<< "$data" + printf "\n" +} + +function cmd::logs::show_wg_events() { + local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" limit="${4:-50}" + + [[ ! -f "$WG_EVENTS_LOG" ]] && return 0 + + local data + data=$(json::wg_events "$WG_EVENTS_LOG" "$filter_name" "$filter_type" "$limit" 2>/dev/null) + + [[ -z "$data" ]] && return 0 + + # Measure column widths + local w_client=16 w_endpoint=16 + while IFS='|' read -r ts client endpoint event count; do + [[ -z "$ts" ]] && continue + (( ${#client} > w_client )) && w_client=${#client} + (( ${#endpoint} > w_endpoint )) && w_endpoint=${#endpoint} + done <<< "$data" + (( w_client += 2 )) + (( w_endpoint += 2 )) + + ui::logs::wg_section_header + while IFS='|' read -r ts client endpoint event count; do + [[ -z "$ts" ]] && continue + ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \ + "$count" "$w_client" "$w_endpoint" + done <<< "$data" + printf "\n" +} + +function cmd::logs::show_merged() { + local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \ + limit="${4:-50}" net_file="${5:-}" + + local fw_data wg_data + fw_data=$(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \ + "$(ctx::clients)" "${net_file:-}" "$limit" 2>/dev/null) + wg_data=$(json::wg_events "$WG_EVENTS_LOG" "$filter_name" "$filter_type" \ + "$limit" 2>/dev/null) + + # Measure widths across both sources + local w_client=16 w_dest=20 + while IFS='|' read -r ts client rest; do + [[ -z "$ts" ]] && continue + (( ${#client} > w_client )) && w_client=${#client} + done < <(echo "$fw_data"; echo "$wg_data") + (( w_client += 2 )) + + # Tag and merge: prefix fw lines with "fw|", wg lines with "wg|" + local merged_data + merged_data=$( + while IFS='|' read -r ts client dest_ip dest_port proto svc count; do + [[ -z "$ts" ]] && continue + echo "fw|${ts}|${client}|${dest_ip}|${dest_port}|${proto}|${svc}|${count}" + done <<< "$fw_data" + while IFS='|' read -r ts client endpoint event count; do + [[ -z "$ts" ]] && continue + echo "wg|${ts}|${client}|${endpoint}|${event}|${count}" + done <<< "$wg_data" + ) + + # Sort by timestamp field 2 + while IFS='|' read -r source ts rest; do + [[ -z "$source" ]] && continue + case "$source" in + fw) + IFS='|' read -r client dest_ip dest_port proto svc count <<< "$rest" + local dest_display + if [[ -n "$svc" ]]; then + [[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})" + else + [[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})" + fi + (( ${#dest_display} > w_dest )) && w_dest=${#dest_display} + ;; + esac + done <<< "$merged_data" + (( w_dest += 2 )) + + while IFS='|' read -r source ts rest; do + [[ -z "$source" ]] && continue + case "$source" in + fw) + IFS='|' read -r client dest_ip dest_port proto svc count <<< "$rest" + ui::watch::fw_row "$ts" "$client" \ + "$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc")" \ + "$w_client" "$w_dest" + ;; + wg) + IFS='|' read -r client endpoint event count <<< "$rest" + ui::watch::wg_row "$ts" "$client" "$endpoint" "$event" \ + "$w_client" "$w_dest" + ;; + esac + done < <(echo "$merged_data" | sort -t'|' -k2,2) + + printf "\n" } function cmd::logs::follow() { local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" local fw_only="${4:-false}" wg_only="${5:-false}" - local filter_peers="${6:-}" - local clients_dir - clients_dir="$(ctx::clients)" - local wg_log="$WG_EVENTS_LOG" - local fw_log="$FW_EVENTS_LOG" - $fw_only && wg_log="" - $wg_only && fw_log="" log::section "WireGuard Live Log (Ctrl+C to stop)" - printf "\n %-20s %-8s %-20s %-25s %s\n" \ - "TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT" - printf " %s\n" "$(printf '─%.0s' {1..90})" + printf "\n" - while IFS="|" read -r source ts client dst_or_endpoint event; do - if [[ "$source" == "fw" ]]; then - local colored_event - case "$event" in - tcp) colored_event="\033[1;33mtcp\033[0m" ;; - udp) colored_event="\033[0;36mudp\033[0m" ;; - icmp) colored_event="\033[0;37micmp\033[0m" ;; - *) colored_event="$event" ;; - esac - printf " %-20s %-8s %-20s %-25s %b\n" \ - "$ts" "firewall" "$client" "$dst_or_endpoint" "$colored_event" - else - local colored_event - case "$event" in - attempt) colored_event="\033[1;31mattempt\033[0m" ;; - handshake) colored_event="\033[1;32mhandshake\033[0m" ;; - *) colored_event="$event" ;; - esac - printf " %-20s %-8s %-20s %-25s %b\n" \ - "$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event" - fi - done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" \ - "$clients_dir" "$filter_peers") + # Delegate to watch command + local watch_args=() + [[ -n "$filter_name" ]] && watch_args+=(--name "$filter_name") + [[ -n "$filter_type" ]] && watch_args+=(--type "$filter_type") + $fw_only && watch_args+=(--restricted) + $wg_only && watch_args+=(--blocked) + + cmd::watch::run "${watch_args[@]}" } function cmd::logs::remove() { @@ -185,7 +309,6 @@ function cmd::logs::remove() { esac done - # Validate — need at least one filter if ! $all && [[ -z "$name" && -z "$before" ]]; then log::error "Specify --name, --before, or --all" cmd::logs::help @@ -198,7 +321,6 @@ function cmd::logs::remove() { filter_ip=$(peers::get_ip "$name") fi - # Build description for confirmation local desc="" $all && desc="all entries" [[ -n "$name" ]] && desc="entries for '${name}'" @@ -209,7 +331,7 @@ function cmd::logs::remove() { if ! $force; then read -r -p "Remove ${desc}? [y/N] " confirm case "$confirm" in - [yY][eE][sS]|[yY]) ;; + [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac fi @@ -233,69 +355,13 @@ function cmd::logs::remove() { log::wg_success "Removed ${total} log entries (wg: ${removed_wg}, fw: ${removed_fw})" } -function cmd::logs::show_wg_events() { - local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" limit="${4:-50}" - - [[ ! -f "$WG_EVENTS_LOG" ]] && return 0 - - printf " WireGuard Events:\n" - printf " %-20s %-20s %-18s %s\n" "TIME" "CLIENT" "ENDPOINT" "EVENT" - printf " %s\n" "$(printf '─%.0s' {1..75})" - - local found=false - while IFS="|" read -r ts client endpoint event; do - [[ -z "$ts" ]] && continue - local colored_event - case "$event" in - attempt*) colored_event="\033[1;31m${event}\033[0m" ;; - handshake*) colored_event="\033[1;32m${event}\033[0m" ;; - *) colored_event="$event" ;; - esac - printf " %-20s %-20s %-18s %b\n" "$ts" "$client" "$endpoint" "$colored_event" - found=true - done < <(json::wg_events "$WG_EVENTS_LOG" "$filter_name" "$filter_type" "$limit") - - $found || printf " —\n" - printf "\n" -} - -function cmd::logs::show_fw_events() { - local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" limit="${4:-50}" - - [[ ! -f "$FW_EVENTS_LOG" ]] && return 0 - - printf " Firewall Drops:\n" - printf " %-20s %-18s %-25s %s\n" "TIME" "CLIENT" "DESTINATION" "PROTOCOL" - printf " %s\n" "$(printf '─%.0s' {1..75})" - - local found=false - while IFS="|" read -r ts client dst proto; do - [[ -z "$ts" ]] && continue - local colored_proto - case "$proto" in - tcp*) colored_proto="\033[1;33m${proto}\033[0m" ;; - udp*) colored_proto="\033[1;36m${proto}\033[0m" ;; - icmp*) colored_proto="\033[0;37m${proto}\033[0m" ;; - *) colored_proto="$proto" ;; - esac - printf " %-20s %-18s %-25s %b\n" "$ts" "$client" "$dst" "$colored_proto" - found=true - done < <(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \ - "$(ctx::clients)" "$limit") - - $found || printf " —\n" - printf "\n" -} - - - function cmd::logs::rotate() { local days=7 force=false while [[ $# -gt 0 ]]; do case "$1" in - --days) days="$2"; shift 2 ;; - --force) force=true; shift ;; + --days) days="$2"; shift 2 ;; + --force) force=true; shift ;; --help) cmd::logs::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac diff --git a/commands/watch.command.sh b/commands/watch.command.sh index 0c54152..69a28fd 100644 --- a/commands/watch.command.sh +++ b/commands/watch.command.sh @@ -11,6 +11,7 @@ function cmd::watch::on_load() { flag::register --blocked flag::register --restricted flag::register --allowed + flag::register --raw } # ============================================ @@ -21,295 +22,27 @@ function cmd::watch::help() { cat < Filter by client name --type Filter by device type - --peers Comma-separated peer names (used internally by group watch) --blocked Show only blocked peer attempts - --allowed Show only handshakes (allowed peers) + --allowed Show only handshakes --restricted Show only firewall drop events + --raw Show raw IPs without service annotation Examples: wgctl watch - wgctl watch --blocked - wgctl watch --allowed - wgctl watch --type phone wgctl watch --name phone-nuno - wgctl watch --name phone-nuno --type phone + wgctl watch --blocked + wgctl watch --type phone EOF } -# ============================================ -# Helpers -# ============================================ - -function cmd::watch::format_event() { - local ts="${1:-}" source="${2:-}" client="${3:-}" - local dest="${4:-}" event="${5:-}" status="${6:-}" - - local event_color - case "$event" in - attempt|drop) event_color="\033[1;31m" ;; - handshake) event_color="\033[1;32m" ;; - *) event_color="\033[0;37m" ;; - esac - - local status_color="" - case "$status" in - blocked) status_color="\033[1;31m" ;; - allowed) status_color="\033[1;32m" ;; - esac - - printf " %-20s %-8s %-22s %-28s ${event_color}%-14s\033[0m ${status_color}%s\033[0m\n" \ - "$ts" "$source" "$client" "${dest:-—}" "$event" "$status" -} - -function cmd::watch::header() { - log::section "wgctl — Live Monitor (Ctrl+C to stop)" - printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \ - "TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT" "STATUS" - printf " %s\n\n" "$(printf '─%.0s' {1..105})" -} - -function cmd::watch::_peer_in_filter() { - local peer="$1" - shift - local peer_set=("$@") - [[ ${#peer_set[@]} -eq 0 ]] && return 0 # no filter = all pass - for p in "${peer_set[@]}"; do - [[ "$p" == "$peer" ]] && return 0 - done - return 1 -} - -# ============================================ -# Handshake Poller -# ============================================ - -function cmd::watch::poll_handshakes() { - local filter_name="${1:-}" filter_type="${2:-}" - local allowed_only="${3:-false}" - local filter_peers="${4:-}" - - 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 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 - cmd::watch::_peer_in_filter "$client_name" "${peer_set[@]}" || 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" "wg" "$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:-}" filter_type="${2:-}" - local blocked_only="${3:-false}" restricted_only="${4:-false}" - local allowed_only="${5:-false}" filter_peers="${6:-}" - - declare -A _WATCH_LAST_ATTEMPT=() - declare -A _WATCH_LAST_FW=() - - local peer_set=() - [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers" - - declare -A _WATCH_LAST_ATTEMPT=() - - # Build ip->name map for fw events - declare -A ip_to_name=() - local conf_file - while IFS= read -r conf_file; do - local name - name=$(basename "$conf_file" .conf) - local ip - ip=$(grep "^Address" "$conf_file" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1) - [[ -n "$ip" && -n "$name" ]] && ip_to_name["$ip"]="$name" - done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null) - - # Source tracker via temp file (persists across subshell iterations) - local source_file - source_file=$(mktemp) - echo "wg" > "$source_file" - - # Cleanup temp file on exit - 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 - - # Handle tail -f file headers - if [[ "$line" == "==> "* ]]; then - if [[ "$line" == *"fw_events"* ]]; then - echo "fw" > "$source_file" - else - echo "wg" > "$source_file" - fi - continue - fi - - local source - source=$(cat "$source_file") - - if [[ "$source" == "wg" ]]; then - $allowed_only && continue # wg events are attempts/blocked - - 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" - - [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue - cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue - - if [[ -n "$filter_type" ]]; then - local conf - conf="$(ctx::clients)/${client}.conf" - [[ -f "$conf" ]] || continue - local ip - ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1) - local subnet - subnet=$(config::subnet_for "$filter_type") - string::starts_with "$ip" "$subnet" || continue - fi - - $restricted_only && { cmd::list::is_restricted "$client" || continue; } - - # Dedup - local now - now=$(date +%s) - local safe_client="${client//[-.]/_}" - local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}" - (( now - last < 30 )) && continue - _WATCH_LAST_ATTEMPT[$safe_client]="$now" - - local formatted_ts - formatted_ts=$(fmt::datetime_iso "$ts") - - cmd::watch::format_event \ - "$formatted_ts" "wg" "$client" "${endpoint:-—}" "$event" "blocked" - - else - # FW event - $allowed_only && continue - $blocked_only && continue # fw drops aren't "blocked peers" per se - - local fw_data - fw_data=$(json::parse_fw_event "$line") - [[ -z "$fw_data" ]] && continue - - local ts src_ip dst_ip dst_port proto - IFS="|" read -r ts src_ip dst_ip dst_port proto <<< "$fw_data" - - [[ -z "$src_ip" ]] && continue - - local fw_key="${src_ip}:${dst_ip}:${dst_port}:${proto}" - local now - now=$(date +%s) - local last_fw="${_WATCH_LAST_FW[$fw_key]:-0}" - - local window=30 - [[ "$proto" == "17" || "$proto" == "udp" ]] && window=10 - [[ "$proto" == "1" || "$proto" == "icmp" ]] && window=5 - - local diff=$(( now - last_fw )) - (( diff < window )) && continue - _WATCH_LAST_FW["$fw_key"]="$now" - - local client="${ip_to_name[$src_ip]:-$src_ip}" - - [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue - cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue - - if [[ -n "$filter_type" ]]; then - local peer_client="${ip_to_name[$src_ip]:-}" - [[ -z "$peer_client" ]] && continue - local conf - conf="$(ctx::clients)/${peer_client}.conf" - [[ -f "$conf" ]] || continue - local ip - ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1) - local subnet - subnet=$(config::subnet_for "$filter_type") - string::starts_with "$ip" "$subnet" || continue - fi - - local dst_str="${dst_ip:-—}" - [[ -n "$dst_port" ]] && dst_str="${dst_ip}:${dst_port}/${proto}" - - local formatted_ts - formatted_ts=$(fmt::datetime_iso "$ts") - - cmd::watch::format_event \ - "$formatted_ts" "fw" "$client" "$dst_str" "drop" "blocked" - fi - done - - rm -f "$source_file" -} - # ============================================ # Run # ============================================ @@ -317,6 +50,7 @@ function cmd::watch::tail_events() { 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 @@ -328,6 +62,7 @@ function cmd::watch::run() { --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" @@ -337,27 +72,207 @@ function cmd::watch::run() { esac done - cmd::watch::header + 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" "$allowed_only" "$filter_peers" + cmd::watch::_poll_handshakes \ + "$filter_name" "$filter_type" "$filter_peers" "$w_client" "$w_dest" sleep 5 done ) & local poller_pid=$! fi - cmd::watch::tail_events \ - "$filter_name" "$filter_type" \ - "$blocked_only" "$restricted_only" \ - "$allowed_only" "$filter_peers" & + # 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_*; echo ''; exit 0" INT TERM + 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 svc_name dest_display + svc_name=$(_resolve_dest "$dest_ip" "$dest_port" "$proto") + dest_display=$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name") + + 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" } \ No newline at end of file diff --git a/core/fmt.sh b/core/fmt.sh index 56a2e4a..fb97281 100644 --- a/core/fmt.sh +++ b/core/fmt.sh @@ -13,6 +13,7 @@ FMT_DATETIME_EU="%d/%m/%Y %H:%M" # 10/05/2026 22:39 # Default — can be overridden in wgctl.conf FMT_DATE="${FMT_DATE_ISO}" FMT_DATETIME="${FMT_DATETIME_ISO}" +FMT_DATETIME_SHORT="%m-%d %H:%M" # Load from config or use default _FMT_DATE_FORMAT="${DATE_FORMAT:-iso}" @@ -62,14 +63,17 @@ function fmt::set_date_format() { iso) FMT_DATE="%Y-%m-%d" FMT_DATETIME="%Y-%m-%d %H:%M" + FMT_DATETIME_SHORT="%m-%d %H:%M" ;; eu) FMT_DATE="%d/%m/%Y" FMT_DATETIME="%d/%m/%Y %H:%M" + FMT_DATETIME_SHORT="%d/%m %H:%M" ;; eu-dash) FMT_DATE="%d-%m-%Y" FMT_DATETIME="%d-%m-%Y %H:%M" + FMT_DATETIME_SHORT="%d-%m %H:%M" ;; *) log::error "Unknown date format: $format" ;; esac diff --git a/core/json.sh b/core/json.sh index b887a6a..705d0f6 100644 --- a/core/json.sh +++ b/core/json.sh @@ -12,8 +12,8 @@ function json::has_key() { python3 "$JSON_HELPER" has_key function json::filter_values() { python3 "$JSON_HELPER" filter_values "$@" name map ip_to_name = {} for conf in glob.glob(f"{clients_dir}/*.conf"): name = os.path.basename(conf).replace('.conf', '') @@ -168,12 +173,40 @@ def fw_events(file, filter_ip, filter_type, clients_dir, limit): if line.startswith('Address'): ip = line.split('=')[1].strip().split('/')[0] ip_to_name[ip] = name - except: + except Exception: pass - + + # Load net services for reverse lookup — independent of rest of function + net_data = {} + if net_file and os.path.exists(net_file): + try: + with open(net_file) as f: + net_data = json.load(f) + except Exception: + pass + + def reverse_lookup(dest_ip, dest_port, proto): + for svc_name, svc in net_data.items(): + if not isinstance(svc, dict): + continue + if svc.get('ip', '') != dest_ip: + continue + ports = svc.get('ports', {}) + if dest_port: + for port_name, port_def in ports.items(): + if not isinstance(port_def, dict): + continue + if (str(port_def.get('port', '')) == str(dest_port) and + port_def.get('proto', 'tcp') == proto): + return f"{svc_name}:{port_name}" + return svc_name + return svc_name + return '' + + # Parse and first-pass dedup (within time window per key) events = [] last_seen = {} - + try: with open(file) as f: for line in f: @@ -184,114 +217,91 @@ def fw_events(file, filter_ip, filter_type, clients_dir, limit): continue if filter_ip and src != filter_ip: continue - dst = e.get('dest_ip', '') - port = e.get('dest_port', '') - proto = e.get('ip.protocol', 0) - key = (src, dst, port, proto) + + proto_num = int(e.get('ip.protocol', 0)) + proto = proto_map.get(proto_num, str(proto_num)) + dst = e.get('dest_ip', '') + port = str(e.get('dest_port', '')) + key = (src, dst, port, proto_num) + ts_str = e.get('timestamp', '') try: - from datetime import datetime ts = datetime.fromisoformat(ts_str).timestamp() - except: + except Exception: ts = 0 + windows = {1: 5, 6: 30, 17: 10} - window = windows.get(proto, 10) + window = windows.get(proto_num, 10) if key in last_seen and (ts - last_seen[key]) < window: continue last_seen[key] = ts events.append(e) - except: + except Exception: pass - except: + except Exception: pass - - # Dedup consecutive same src+dst+port within 60s with count + + # Second-pass dedup consecutive same events with count deduped = [] counts = [] for e in events: ts_str = e.get('timestamp', '') try: - from datetime import datetime ts = datetime.fromisoformat(ts_str).timestamp() - except: + except Exception: ts = 0 - src = e.get('src_ip', '') - dst = e.get('dest_ip', '') - port = e.get('dest_port', '') - proto = e.get('ip.protocol', 0) - key = (src, dst, port, proto) - - if deduped and counts: + + src = e.get('src_ip', '') + dst = e.get('dest_ip', '') + port = str(e.get('dest_port', '')) + proto_num = int(e.get('ip.protocol', 0)) + key = (src, dst, port, proto_num) + + if deduped: prev = deduped[-1] try: - prev_ts = datetime.fromisoformat(prev.get('timestamp','')).timestamp() - except: + prev_ts = datetime.fromisoformat(prev.get('timestamp', '')).timestamp() + except Exception: prev_ts = 0 - prev_key = (prev.get('src_ip',''), prev.get('dest_ip',''), - prev.get('dest_port',''), prev.get('ip.protocol',0)) - if key == prev_key and (ts - prev_ts) < 60: + prev_key = ( + prev.get('src_ip', ''), + prev.get('dest_ip', ''), + str(prev.get('dest_port', '')), + int(prev.get('ip.protocol', 0)) + ) + if key == prev_key and (ts - prev_ts) < 300: counts[-1] += 1 continue - + deduped.append(e) counts.append(1) - - grouped = [] - group_counts = [] - for e in deduped: - ts_str = e.get('timestamp', '') - try: - from datetime import datetime - dt = datetime.fromisoformat(ts_str) - # Truncate to minute for grouping - minute_key = dt.strftime('%Y-%m-%d %H:%M') - except: - minute_key = ts_str[:16] - - src = e.get('src_ip', '') - dst = e.get('dest_ip', '') - port = e.get('dest_port', '') - proto = e.get('ip.protocol', 0) - key = (minute_key, src, dst, proto) # group within same minute - - if grouped and group_counts: - prev = grouped[-1] - try: - prev_dt = datetime.fromisoformat(prev.get('timestamp','')) - prev_minute = prev_dt.strftime('%Y-%m-%d %H:%M') - except: - prev_minute = '' - prev_key = (prev_minute, prev.get('src_ip',''), - prev.get('dest_ip',''), prev.get('ip.protocol',0)) - if key == prev_key: - group_counts[-1] += 1 - continue - - grouped.append(e) - group_counts.append(1) - - for e, count in zip(deduped[-int(limit):], counts[-int(limit):]): - ts = e.get('timestamp', '') - try: - from datetime import datetime - dt = datetime.fromisoformat(ts) - ts = dt.strftime(DATETIME_FMT) - except: - pass - src = e.get('src_ip', '—') - dst = e.get('dest_ip', '—') - port = e.get('dest_port', '') - proto_num = e.get('ip.protocol', 0) + + limit = int(limit) if limit else 50 + for e, count in list(zip(deduped, counts))[-limit:]: + ts_str = e.get('timestamp', '') + src = e.get('src_ip', '') + dst = e.get('dest_ip', '') + port = str(e.get('dest_port', '')) + proto_num = int(e.get('ip.protocol', 0)) proto = proto_map.get(proto_num, str(proto_num)) - dst_str = f"{dst}:{port}" if port else dst client = ip_to_name.get(src, src) - if filter_type and not client.startswith(filter_type + '-'): - continue - count_str = f" (x{count})" if count > 1 else "" - print(f"{ts}|{client}|{dst_str}|{proto}{count_str}") - + svc_name = reverse_lookup(dst, port, proto) + + try: + dt = datetime.fromisoformat(ts_str) + ts_fmt = dt.strftime(DATETIME_FMT) + except Exception: + ts_fmt = ts_str + + print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}") + def wg_events(file, filter_client, filter_type, limit): - """Format WireGuard events from events.log with dedup""" + """ + Format WireGuard events with dedup and counts. + Output per line: ts|client|endpoint|event|count + """ + from datetime import datetime + events = [] try: with open(file) as f: @@ -306,54 +316,57 @@ def wg_events(file, filter_client, filter_type, limit): if filter_type and not client.startswith(filter_type + '-'): continue events.append(e) - except: + except Exception: pass - except: + except Exception: pass - + # Dedup consecutive same client+event+endpoint within 60s deduped = [] - counts = [] + counts = [] for e in events: - ts_str = e.get('timestamp', '') + ts_str = e.get('timestamp', '') try: - from datetime import datetime ts = datetime.fromisoformat(ts_str).timestamp() - except: + except Exception: ts = 0 - client = e.get('client', '') - event = e.get('event', '') + client = e.get('client', '') + event = e.get('event', '') endpoint = e.get('endpoint', '') - key = (client, event, endpoint[:15]) - - if deduped and counts: - prev = deduped[-1] + key = (client, event, endpoint[:15]) + + if deduped: + prev = deduped[-1] prev_ts_str = prev.get('timestamp', '') try: prev_ts = datetime.fromisoformat(prev_ts_str).timestamp() - except: + except Exception: prev_ts = 0 - prev_key = (prev.get('client',''), prev.get('event',''), prev.get('endpoint','')[:15]) - if key == prev_key and (ts - prev_ts) < 60: + prev_key = ( + prev.get('client', ''), + prev.get('event', ''), + prev.get('endpoint', '')[:15] + ) + if key == prev_key and (ts - prev_ts) < 300: counts[-1] += 1 continue - + deduped.append(e) counts.append(1) - - for e, count in zip(deduped[-int(limit):], counts[-int(limit):]): - ts = e.get('timestamp', '') + + limit = int(limit) if limit else 50 + for e, count in list(zip(deduped, counts))[-limit:]: + ts_str = e.get('timestamp', '') + client = e.get('client', '') + endpoint = e.get('endpoint', '') + event = e.get('event', '') try: - from datetime import datetime - dt = datetime.fromisoformat(ts) - ts = dt.strftime(DATETIME_FMT) - except: - pass - client = e.get('client', '—') - endpoint = e.get('endpoint', '—') - event = e.get('event', '—') - count_str = f" (x{count})" if count > 1 else "" - print(f"{ts}|{client}|{endpoint}|{event}{count_str}") + dt = datetime.fromisoformat(ts_str) + ts_fmt = dt.strftime('%d/%m %H:%M') + except Exception: + ts_fmt = ts_str + + print(f"{ts_fmt}|{client}|{endpoint}|{event}|{count}") def format_fw_event(line, clients_dir): """Format a single fw_event line""" @@ -2448,24 +2461,115 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file, except Exception: pass - def reverse_lookup(dest_ip, dest_port, proto): - for svc_name, svc in net_data.items(): - if not isinstance(svc, dict): - continue - if svc.get('ip', '') != dest_ip: - continue - ports = svc.get('ports', {}) - if dest_port: - for port_name, port_def in ports.items(): - if not isinstance(port_def, dict): +def reverse_lookup(dest_ip, dest_port, proto): + for svc_name, svc in net_data.items(): + if not isinstance(svc, dict): + continue + if svc.get('ip', '') != dest_ip: + continue + ports = svc.get('ports', {}) + if dest_port: + for port_name, port_def in ports.items(): + if not isinstance(port_def, dict): + continue + if (str(port_def.get('port', '')) == str(dest_port) and + port_def.get('proto', 'tcp') == proto): + return f"{svc_name}:{port_name}" + return svc_name + return svc_name + return '' + + # Parse and first-pass dedup (within time window per key) + events = [] + last_seen = {} + + try: + with open(file) as f: + for line in f: + try: + e = json.loads(line.strip()) + src = e.get('src_ip', '') + if not src: continue - if (str(port_def.get('port', '')) == dest_port and - port_def.get('proto', 'tcp') == proto): - return f"{svc_name}:{port_name}" - return svc_name - else: - return svc_name - return '' + if filter_ip and src != filter_ip: + continue + + proto_num = int(e.get('ip.protocol', 0)) + proto = proto_map.get(proto_num, str(proto_num)) + dst = e.get('dest_ip', '') + port = str(e.get('dest_port', '')) + key = (src, dst, port, proto_num) + + ts_str = e.get('timestamp', '') + try: + ts = datetime.fromisoformat(ts_str).timestamp() + except Exception: + ts = 0 + + windows = {1: 5, 6: 30, 17: 10} + window = windows.get(proto_num, 10) + if key in last_seen and (ts - last_seen[key]) < window: + continue + last_seen[key] = ts + events.append(e) + except Exception: + pass + except Exception: + pass + + # Second-pass dedup consecutive same events with count + deduped = [] + counts = [] + for e in events: + ts_str = e.get('timestamp', '') + try: + ts = datetime.fromisoformat(ts_str).timestamp() + except Exception: + ts = 0 + + src = e.get('src_ip', '') + dst = e.get('dest_ip', '') + port = str(e.get('dest_port', '')) + proto_num = int(e.get('ip.protocol', 0)) + key = (src, dst, port, proto_num) + + if deduped: + prev = deduped[-1] + try: + prev_ts = datetime.fromisoformat(prev.get('timestamp', '')).timestamp() + except Exception: + prev_ts = 0 + prev_key = ( + prev.get('src_ip', ''), + prev.get('dest_ip', ''), + str(prev.get('dest_port', '')), + int(prev.get('ip.protocol', 0)) + ) + if key == prev_key and (ts - prev_ts) < 300: + counts[-1] += 1 + continue + + deduped.append(e) + counts.append(1) + + limit = int(limit) if limit else 50 + for e, count in list(zip(deduped, counts))[-limit:]: + ts_str = e.get('timestamp', '') + src = e.get('src_ip', '') + dst = e.get('dest_ip', '') + port = str(e.get('dest_port', '')) + proto_num = int(e.get('ip.protocol', 0)) + proto = proto_map.get(proto_num, str(proto_num)) + client = ip_to_name.get(src, src) + svc_name = reverse_lookup(dst, port, proto) + + try: + dt = datetime.fromisoformat(ts_str) + ts_fmt = dt.strftime(DATETIME_FMT) + except Exception: + ts_fmt = ts_str + + print(f"{ts_fmt}|{client}|{dst}|{port}|{proto}|{svc_name}|{count}") def make_dest_display(dest_ip, dest_port, proto, svc_name): if svc_name: @@ -2563,8 +2667,8 @@ commands = { 'filter_values': lambda args: filter_values(args[0], args[1], args[2]), 'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]), 'events_for': lambda args: events_for(args[0], args[1], args[2]), - 'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]), - 'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]), + 'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4], args[5] if len(args) > 5 else '50'), + 'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3] if len(args) > 3 else '50'), 'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]), 'format_wg_event': lambda args: format_wg_event(sys.stdin.read()), 'remove_events': lambda args: remove_events(args[0], args[1]), diff --git a/modules/config.module.sh b/modules/config.module.sh index 1e54c7d..3e39a28 100644 --- a/modules/config.module.sh +++ b/modules/config.module.sh @@ -24,6 +24,7 @@ declare -g _ACTIVITY_CURRENT_LOW_BYTES="${ACTIVITY_CURRENT_LOW_BYTES:-1000000}" declare -g _ACTIVITY_CURRENT_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}" declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}" + function config::_init_defaults() { _WG_INTERFACE="${WG_INTERFACE:-wg0}" _WG_DNS="${WG_DNS:-10.0.0.103}" diff --git a/modules/ui/logs.module.sh b/modules/ui/logs.module.sh new file mode 100644 index 0000000..279a8f4 --- /dev/null +++ b/modules/ui/logs.module.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# ui/logs.module.sh — rendering for logs and watch data + +function ui::logs::build_dest() { + local dest_ip="${1:-}" dest_port="${2:-}" proto="${3:-}" svc="${4:-}" + if [[ -n "$svc" ]]; then + [[ -n "$dest_port" ]] && echo "${svc}/${proto}" || echo "${svc} (${proto})" + else + [[ -n "$dest_port" ]] && echo "${dest_ip}:${dest_port}/${proto}" || echo "${dest_ip} (${proto})" + fi +} + +function ui::logs::fw_section_header() { + printf " Firewall Drops\n" + printf " %s\n" "$(printf '─%.0s' {1..42})" +} + +function ui::logs::wg_section_header() { + printf " WireGuard Events\n" + printf " %s\n" "$(printf '─%.0s' {1..42})" +} + +function ui::logs::fw_section_header_table() { + printf " Firewall Drops:\n" + printf " %-20s %-18s %-25s %s\n" "TIME" "CLIENT" "DESTINATION" "PROTOCOL" + printf " %s\n" "$(printf '─%.0s' {1..75})" +} + +function ui::logs::wg_section_header_table() { + printf " WireGuard Events:\n" + printf " %-20s %-20s %-18s %s\n" "TIME" "CLIENT" "ENDPOINT" "EVENT" + printf " %s\n" "$(printf '─%.0s' {1..75})" +} + +function ui::logs::fw_row() { + local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \ + proto="${5:-}" svc_name="${6:-}" count="${7:-1}" \ + w_client="${8:-20}" w_dest="${9:-30}" + local dest_display + dest_display=$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name") + local count_suffix="" + [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" + local client_pad dest_pad_n + client_pad=$(printf "%-${w_client}s" "$client") + dest_pad_n=$(( w_dest - ${#dest_display} )) + [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 + printf " %s %s \033[1;31m→\033[0m %s%*s%b\n" \ + "$ts" "$client_pad" "$dest_display" "$dest_pad_n" "" "$count_suffix" +} + +function ui::logs::fw_row_table() { + local ts="${1:-}" client="${2:-}" dst="${3:-}" proto="${4:-}" count="${5:-1}" + local count_str="" + [[ "$count" -gt 1 ]] && count_str=" (x${count})" + printf " %-20s %-18s %-25s %s%s\n" "$ts" "$client" "$dst" "$proto" "$count_str" +} + +function ui::logs::wg_row() { + local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" count="${5:-1}" \ + w_client="${6:-20}" w_endpoint="${7:-20}" + local event_color + case "$event" in + handshake) event_color="\033[1;32m" ;; + attempt) event_color="\033[1;31m" ;; + *) event_color="\033[0;37m" ;; + esac + local count_suffix="" + [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" + local client_pad endpoint_pad_n + client_pad=$(printf "%-${w_client}s" "$client") + endpoint_pad_n=$(( w_endpoint - ${#endpoint} )) + [[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0 + printf " %s %s %s%*s %b%s\033[0m%b\n" \ + "$ts" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \ + "$event_color" "$event" "$count_suffix" +} + +function ui::logs::wg_row_table() { + local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" count="${5:-1}" + local count_str="" + [[ "$count" -gt 1 ]] && count_str=" (x${count})" + local event_colored + case "$event" in + attempt*) event_colored="\033[1;31m${event}\033[0m" ;; + handshake*) event_colored="\033[1;32m${event}\033[0m" ;; + *) event_colored="$event" ;; + esac + printf " %-20s %-20s %-18s %b%s\n" "$ts" "$client" "$endpoint" "$event_colored" "$count_str" +} + +_UI_WATCH_FW_COLOR="\033[1;31m" +_UI_WATCH_WG_COLOR="\033[1;32m" + +function ui::watch::fw_row() { + local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \ + w_client="${4:-20}" w_dest="${5:-18}" + + local ts_pad + ts_pad=$(printf "%-11s" "$ts") + + local src + src=$(ui::pad_mb "${_UI_WATCH_FW_COLOR}fw\033[0m" 2) + local client_pad dest_pad_n + client_pad=$(printf "%-${w_client}s" "$client") + dest_pad_n=$(( w_dest - ${#dest_display} )) + [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 + # echo "DEBUG fw: ts_bytes=${#ts} src_bytes=${#src} client='$client'(${#client}) client_pad_bytes=${#client_pad}" >&2 + printf " %s %b %s \033[1;31m→\033[0m %s%*s \033[1;31mdrop\033[0m\n" \ + "$ts_pad" "$src" "$client_pad" "$dest_display" "$dest_pad_n" "" +} + +function ui::watch::wg_row() { + local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ + w_client="${5:-20}" w_endpoint="${6:-18}" + + local ts_pad + ts_pad=$(printf "%-11s" "$ts") + + local event_color + case "$event" in + handshake) event_color="\033[1;32m" ;; + attempt) event_color="\033[1;31m" ;; + *) event_color="\033[0;37m" ;; + esac + local src + src=$(ui::pad_mb "${_UI_WATCH_WG_COLOR}wg\033[0m" 2) + + case "$event" in + handshake) src="\033[1;32m" ;; # green + attempt) src="\033[1;31m" ;; # red + *) src="\033[0;37m" ;; # gray + esac + local src_colored="${src}wg\033[0m" + + local client_pad endpoint_pad_n + client_pad=$(printf "%-${w_client}s" "$client") + endpoint_pad_n=$(( w_endpoint - ${#endpoint} )) + [[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0 + # echo "DEBUG wg: ts_bytes=${#ts} src_bytes=${#src} client='$client'(${#client}) client_pad_bytes=${#client_pad}" >&2 + printf " %s %b %s %s%*s %b%s\033[0m\n" \ + "$ts_pad" "$src_colored" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \ + "$event_color" "$event" +} + +function ui::watch::header_table() { + printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \ + "TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT" "STATUS" + printf " %s\n\n" "$(printf '─%.0s' {1..105})" +} + +function ui::watch::fw_row_table() { + local ts="${1:-}" client="${2:-}" dest="${3:-}" event="${4:-}" status="${5:-}" + printf " %-20s %-8s %-22s %-28s \033[1;31m%-14s\033[0m %s\n" \ + "$ts" "firewall" "$client" "$dest" "$event" "$status" +} + +function ui::watch::wg_row_table() { + local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" status="${5:-}" + local event_color + case "$event" in + handshake) event_color="\033[1;32m" ;; + attempt) event_color="\033[1;31m" ;; + *) event_color="\033[0;37m" ;; + esac + printf " %-20s %-8s %-22s %-28s %b%-14s\033[0m %s\n" \ + "$ts" "wireguard" "$client" "$endpoint" "$event_color" "$event" "$status" +} \ No newline at end of file