feat: tableless logs/watch layout with service annotations
- wgctl logs: tableless layout, fw/wg sections, --merged flag, --raw flag - wgctl watch: tableless layout, service annotations, colored fw/wg labels - wgctl rule list: tableless with +N/-N/+all indicators, inline extends - wgctl activity: transfer totals and firewall drops per peer - ui/logs.module.sh: fw_row, wg_row, watch rows, table versions kept - ui/rule.module.sh: list_row, list_group_header, list_base_header - fmt.sh: FMT_DATETIME_SHORT, updated fmt::set_date_format - json_helper.py: fw_events with service annotation, wg_events with count
This commit is contained in:
parent
57e08e88c4
commit
4dcf98b128
7 changed files with 790 additions and 533 deletions
|
|
@ -11,10 +11,12 @@ function cmd::logs::on_load() {
|
||||||
flag::register --fw
|
flag::register --fw
|
||||||
flag::register --wg
|
flag::register --wg
|
||||||
flag::register --follow
|
flag::register --follow
|
||||||
|
flag::register --merged
|
||||||
flag::register --all
|
flag::register --all
|
||||||
flag::register --before
|
flag::register --before
|
||||||
flag::register --force
|
flag::register --force
|
||||||
flag::register --days
|
flag::register --days
|
||||||
|
flag::register --raw
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::help() {
|
function cmd::logs::help() {
|
||||||
|
|
@ -34,7 +36,9 @@ Options for show:
|
||||||
--limit <n> Max results per source (default: 50)
|
--limit <n> Max results per source (default: 50)
|
||||||
--fw Show only firewall drops
|
--fw Show only firewall drops
|
||||||
--wg Show only WireGuard events
|
--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:
|
Options for remove:
|
||||||
--name <name> Remove entries for specific peer
|
--name <name> Remove entries for specific peer
|
||||||
|
|
@ -52,12 +56,10 @@ Examples:
|
||||||
wgctl logs
|
wgctl logs
|
||||||
wgctl logs --name phone-nuno
|
wgctl logs --name phone-nuno
|
||||||
wgctl logs --fw --limit 100
|
wgctl logs --fw --limit 100
|
||||||
|
wgctl logs --merged
|
||||||
wgctl logs --follow
|
wgctl logs --follow
|
||||||
wgctl logs remove --name phone-nuno
|
wgctl logs remove --name phone-nuno
|
||||||
wgctl logs remove --all --force
|
wgctl logs rotate --days 30
|
||||||
wgctl logs remove --fw --before 1
|
|
||||||
wgctl logs rotate
|
|
||||||
wgctl logs rotate --days 30 --force
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +86,7 @@ function cmd::logs::run() {
|
||||||
|
|
||||||
function cmd::logs::show() {
|
function cmd::logs::show() {
|
||||||
local name="" type="" limit=50
|
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
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -93,7 +95,9 @@ function cmd::logs::show() {
|
||||||
--limit) limit="$2"; shift 2 ;;
|
--limit) limit="$2"; shift 2 ;;
|
||||||
--fw) fw_only=true; shift ;;
|
--fw) fw_only=true; shift ;;
|
||||||
--wg) wg_only=true; shift ;;
|
--wg) wg_only=true; shift ;;
|
||||||
|
--merged) merged=true; shift ;;
|
||||||
--follow|-f) follow=true; shift ;;
|
--follow|-f) follow=true; shift ;;
|
||||||
|
--raw) raw=true; shift ;;
|
||||||
--help) cmd::logs::help; return ;;
|
--help) cmd::logs::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
|
|
@ -117,51 +121,171 @@ function cmd::logs::show() {
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local net_file=""
|
||||||
|
$raw || net_file="$(ctx::net)"
|
||||||
|
|
||||||
log::section "WireGuard Activity Log"
|
log::section "WireGuard Activity Log"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
$wg_only || cmd::logs::show_fw_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"
|
$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() {
|
function cmd::logs::follow() {
|
||||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}"
|
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}"
|
||||||
local fw_only="${4:-false}" wg_only="${5:-false}"
|
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)"
|
log::section "WireGuard Live Log (Ctrl+C to stop)"
|
||||||
printf "\n %-20s %-8s %-20s %-25s %s\n" \
|
printf "\n"
|
||||||
"TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT"
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..90})"
|
|
||||||
|
|
||||||
while IFS="|" read -r source ts client dst_or_endpoint event; do
|
# Delegate to watch command
|
||||||
if [[ "$source" == "fw" ]]; then
|
local watch_args=()
|
||||||
local colored_event
|
[[ -n "$filter_name" ]] && watch_args+=(--name "$filter_name")
|
||||||
case "$event" in
|
[[ -n "$filter_type" ]] && watch_args+=(--type "$filter_type")
|
||||||
tcp) colored_event="\033[1;33mtcp\033[0m" ;;
|
$fw_only && watch_args+=(--restricted)
|
||||||
udp) colored_event="\033[0;36mudp\033[0m" ;;
|
$wg_only && watch_args+=(--blocked)
|
||||||
icmp) colored_event="\033[0;37micmp\033[0m" ;;
|
|
||||||
*) colored_event="$event" ;;
|
cmd::watch::run "${watch_args[@]}"
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::remove() {
|
function cmd::logs::remove() {
|
||||||
|
|
@ -185,7 +309,6 @@ function cmd::logs::remove() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Validate — need at least one filter
|
|
||||||
if ! $all && [[ -z "$name" && -z "$before" ]]; then
|
if ! $all && [[ -z "$name" && -z "$before" ]]; then
|
||||||
log::error "Specify --name, --before, or --all"
|
log::error "Specify --name, --before, or --all"
|
||||||
cmd::logs::help
|
cmd::logs::help
|
||||||
|
|
@ -198,7 +321,6 @@ function cmd::logs::remove() {
|
||||||
filter_ip=$(peers::get_ip "$name")
|
filter_ip=$(peers::get_ip "$name")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build description for confirmation
|
|
||||||
local desc=""
|
local desc=""
|
||||||
$all && desc="all entries"
|
$all && desc="all entries"
|
||||||
[[ -n "$name" ]] && desc="entries for '${name}'"
|
[[ -n "$name" ]] && desc="entries for '${name}'"
|
||||||
|
|
@ -209,7 +331,7 @@ function cmd::logs::remove() {
|
||||||
if ! $force; then
|
if ! $force; then
|
||||||
read -r -p "Remove ${desc}? [y/N] " confirm
|
read -r -p "Remove ${desc}? [y/N] " confirm
|
||||||
case "$confirm" in
|
case "$confirm" in
|
||||||
[yY][eE][sS]|[yY]) ;;
|
[yY]*) ;;
|
||||||
*) log::info "Aborted"; return 0 ;;
|
*) log::info "Aborted"; return 0 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
@ -233,62 +355,6 @@ function cmd::logs::remove() {
|
||||||
log::wg_success "Removed ${total} log entries (wg: ${removed_wg}, fw: ${removed_fw})"
|
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() {
|
function cmd::logs::rotate() {
|
||||||
local days=7 force=false
|
local days=7 force=false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ function cmd::watch::on_load() {
|
||||||
flag::register --blocked
|
flag::register --blocked
|
||||||
flag::register --restricted
|
flag::register --restricted
|
||||||
flag::register --allowed
|
flag::register --allowed
|
||||||
|
flag::register --raw
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -21,295 +22,27 @@ function cmd::watch::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl watch [options]
|
Usage: wgctl watch [options]
|
||||||
|
|
||||||
Live monitor of WireGuard activity. Shows:
|
Live monitor of WireGuard activity.
|
||||||
- Handshakes from connected peers (green)
|
- Handshakes from connected peers (green)
|
||||||
- Connection attempts from blocked peers (red, SOURCE: wg)
|
- Connection attempts from blocked peers (red)
|
||||||
- Firewall drops from rule-restricted peers (red, SOURCE: fw)
|
- Firewall drops from restricted peers (red)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Filter by client name
|
--name <name> Filter by client name
|
||||||
--type <type> Filter by device type
|
--type <type> Filter by device type
|
||||||
--peers <list> Comma-separated peer names (used internally by group watch)
|
|
||||||
--blocked Show only blocked peer attempts
|
--blocked Show only blocked peer attempts
|
||||||
--allowed Show only handshakes (allowed peers)
|
--allowed Show only handshakes
|
||||||
--restricted Show only firewall drop events
|
--restricted Show only firewall drop events
|
||||||
|
--raw Show raw IPs without service annotation
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl watch
|
wgctl watch
|
||||||
wgctl watch --blocked
|
|
||||||
wgctl watch --allowed
|
|
||||||
wgctl watch --type phone
|
|
||||||
wgctl watch --name phone-nuno
|
wgctl watch --name phone-nuno
|
||||||
wgctl watch --name phone-nuno --type phone
|
wgctl watch --blocked
|
||||||
|
wgctl watch --type phone
|
||||||
EOF
|
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
|
# Run
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -317,6 +50,7 @@ function cmd::watch::tail_events() {
|
||||||
function cmd::watch::run() {
|
function cmd::watch::run() {
|
||||||
local filter_name="" filter_type="" filter_peers=""
|
local filter_name="" filter_type="" filter_peers=""
|
||||||
local blocked_only=false allowed_only=false restricted_only=false
|
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
|
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 ;;
|
--blocked) blocked_only=true; shift ;;
|
||||||
--allowed) allowed_only=true; shift ;;
|
--allowed) allowed_only=true; shift ;;
|
||||||
--restricted) restricted_only=true; shift ;;
|
--restricted) restricted_only=true; shift ;;
|
||||||
|
--raw) raw=true; shift ;;
|
||||||
--help) cmd::watch::help; return ;;
|
--help) cmd::watch::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
|
|
@ -337,27 +72,207 @@ function cmd::watch::run() {
|
||||||
esac
|
esac
|
||||||
done
|
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
|
if ! $blocked_only && ! $restricted_only; then
|
||||||
(
|
(
|
||||||
while true; do
|
while true; do
|
||||||
cmd::watch::poll_handshakes \
|
cmd::watch::_poll_handshakes \
|
||||||
"$filter_name" "$filter_type" "$allowed_only" "$filter_peers"
|
"$filter_name" "$filter_type" "$filter_peers" "$w_client" "$w_dest"
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
) &
|
) &
|
||||||
local poller_pid=$!
|
local poller_pid=$!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd::watch::tail_events \
|
# Event tailer (background)
|
||||||
"$filter_name" "$filter_type" \
|
cmd::watch::_tail_events \
|
||||||
"$blocked_only" "$restricted_only" \
|
"$filter_name" "$filter_type" "$filter_peers" \
|
||||||
"$allowed_only" "$filter_peers" &
|
"$blocked_only" "$restricted_only" "$allowed_only" \
|
||||||
|
"$net_file" "$w_client" "$w_dest" &
|
||||||
local tailer_pid=$!
|
local tailer_pid=$!
|
||||||
|
|
||||||
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
|
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
|
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"
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ FMT_DATETIME_EU="%d/%m/%Y %H:%M" # 10/05/2026 22:39
|
||||||
# Default — can be overridden in wgctl.conf
|
# Default — can be overridden in wgctl.conf
|
||||||
FMT_DATE="${FMT_DATE_ISO}"
|
FMT_DATE="${FMT_DATE_ISO}"
|
||||||
FMT_DATETIME="${FMT_DATETIME_ISO}"
|
FMT_DATETIME="${FMT_DATETIME_ISO}"
|
||||||
|
FMT_DATETIME_SHORT="%m-%d %H:%M"
|
||||||
|
|
||||||
# Load from config or use default
|
# Load from config or use default
|
||||||
_FMT_DATE_FORMAT="${DATE_FORMAT:-iso}"
|
_FMT_DATE_FORMAT="${DATE_FORMAT:-iso}"
|
||||||
|
|
@ -62,14 +63,17 @@ function fmt::set_date_format() {
|
||||||
iso)
|
iso)
|
||||||
FMT_DATE="%Y-%m-%d"
|
FMT_DATE="%Y-%m-%d"
|
||||||
FMT_DATETIME="%Y-%m-%d %H:%M"
|
FMT_DATETIME="%Y-%m-%d %H:%M"
|
||||||
|
FMT_DATETIME_SHORT="%m-%d %H:%M"
|
||||||
;;
|
;;
|
||||||
eu)
|
eu)
|
||||||
FMT_DATE="%d/%m/%Y"
|
FMT_DATE="%d/%m/%Y"
|
||||||
FMT_DATETIME="%d/%m/%Y %H:%M"
|
FMT_DATETIME="%d/%m/%Y %H:%M"
|
||||||
|
FMT_DATETIME_SHORT="%d/%m %H:%M"
|
||||||
;;
|
;;
|
||||||
eu-dash)
|
eu-dash)
|
||||||
FMT_DATE="%d-%m-%Y"
|
FMT_DATE="%d-%m-%Y"
|
||||||
FMT_DATETIME="%d-%m-%Y %H:%M"
|
FMT_DATETIME="%d-%m-%Y %H:%M"
|
||||||
|
FMT_DATETIME_SHORT="%d-%m %H:%M"
|
||||||
;;
|
;;
|
||||||
*) log::error "Unknown date format: $format" ;;
|
*) log::error "Unknown date format: $format" ;;
|
||||||
esac
|
esac
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ function json::has_key() { python3 "$JSON_HELPER" has_key
|
||||||
function json::filter_values() { python3 "$JSON_HELPER" filter_values "$@" </dev/null; }
|
function json::filter_values() { python3 "$JSON_HELPER" filter_values "$@" </dev/null; }
|
||||||
function json::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; }
|
function json::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; }
|
||||||
function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </dev/null; }
|
function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </dev/null; }
|
||||||
function json::fw_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" fw_events "$@" </dev/null; }
|
function json::fw_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME_SHORT" python3 "$JSON_HELPER" fw_events "$@" </dev/null; }
|
||||||
function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" wg_events "$@" </dev/null; }
|
function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME_SHORT" python3 "$JSON_HELPER" wg_events "$@" </dev/null; }
|
||||||
function json::format_fw_event() { echo "$1" | python3 "$JSON_HELPER" format_fw_event "$2"; }
|
function json::format_fw_event() { echo "$1" | python3 "$JSON_HELPER" format_fw_event "$2"; }
|
||||||
function json::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
|
function json::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
|
||||||
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
|
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
|
||||||
|
|
|
||||||
|
|
@ -154,11 +154,16 @@ def events_for(file, ip, limit):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
def fw_events(file, filter_ip, filter_type, clients_dir, net_file, limit):
|
||||||
"""Format firewall drop events with dedup and counts"""
|
"""
|
||||||
|
Format firewall drop events with dedup, counts, and service annotation.
|
||||||
|
Output per line: ts|client|dest_ip|dest_port|proto|service_name|count
|
||||||
|
"""
|
||||||
import glob
|
import glob
|
||||||
|
from datetime import datetime
|
||||||
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
||||||
|
|
||||||
|
# Build ip->name map
|
||||||
ip_to_name = {}
|
ip_to_name = {}
|
||||||
for conf in glob.glob(f"{clients_dir}/*.conf"):
|
for conf in glob.glob(f"{clients_dir}/*.conf"):
|
||||||
name = os.path.basename(conf).replace('.conf', '')
|
name = os.path.basename(conf).replace('.conf', '')
|
||||||
|
|
@ -168,9 +173,37 @@ def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
||||||
if line.startswith('Address'):
|
if line.startswith('Address'):
|
||||||
ip = line.split('=')[1].strip().split('/')[0]
|
ip = line.split('=')[1].strip().split('/')[0]
|
||||||
ip_to_name[ip] = name
|
ip_to_name[ip] = name
|
||||||
except:
|
except Exception:
|
||||||
pass
|
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 = []
|
events = []
|
||||||
last_seen = {}
|
last_seen = {}
|
||||||
|
|
||||||
|
|
@ -184,114 +217,91 @@ def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
||||||
continue
|
continue
|
||||||
if filter_ip and src != filter_ip:
|
if filter_ip and src != filter_ip:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
proto_num = int(e.get('ip.protocol', 0))
|
||||||
|
proto = proto_map.get(proto_num, str(proto_num))
|
||||||
dst = e.get('dest_ip', '')
|
dst = e.get('dest_ip', '')
|
||||||
port = e.get('dest_port', '')
|
port = str(e.get('dest_port', ''))
|
||||||
proto = e.get('ip.protocol', 0)
|
key = (src, dst, port, proto_num)
|
||||||
key = (src, dst, port, proto)
|
|
||||||
ts_str = e.get('timestamp', '')
|
ts_str = e.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
|
||||||
ts = datetime.fromisoformat(ts_str).timestamp()
|
ts = datetime.fromisoformat(ts_str).timestamp()
|
||||||
except:
|
except Exception:
|
||||||
ts = 0
|
ts = 0
|
||||||
|
|
||||||
windows = {1: 5, 6: 30, 17: 10}
|
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:
|
if key in last_seen and (ts - last_seen[key]) < window:
|
||||||
continue
|
continue
|
||||||
last_seen[key] = ts
|
last_seen[key] = ts
|
||||||
events.append(e)
|
events.append(e)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Dedup consecutive same src+dst+port within 60s with count
|
# Second-pass dedup consecutive same events with count
|
||||||
deduped = []
|
deduped = []
|
||||||
counts = []
|
counts = []
|
||||||
for e in events:
|
for e in events:
|
||||||
ts_str = e.get('timestamp', '')
|
ts_str = e.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
|
||||||
ts = datetime.fromisoformat(ts_str).timestamp()
|
ts = datetime.fromisoformat(ts_str).timestamp()
|
||||||
except:
|
except Exception:
|
||||||
ts = 0
|
ts = 0
|
||||||
|
|
||||||
src = e.get('src_ip', '')
|
src = e.get('src_ip', '')
|
||||||
dst = e.get('dest_ip', '')
|
dst = e.get('dest_ip', '')
|
||||||
port = e.get('dest_port', '')
|
port = str(e.get('dest_port', ''))
|
||||||
proto = e.get('ip.protocol', 0)
|
proto_num = int(e.get('ip.protocol', 0))
|
||||||
key = (src, dst, port, proto)
|
key = (src, dst, port, proto_num)
|
||||||
|
|
||||||
if deduped and counts:
|
if deduped:
|
||||||
prev = deduped[-1]
|
prev = deduped[-1]
|
||||||
try:
|
try:
|
||||||
prev_ts = datetime.fromisoformat(prev.get('timestamp','')).timestamp()
|
prev_ts = datetime.fromisoformat(prev.get('timestamp', '')).timestamp()
|
||||||
except:
|
except Exception:
|
||||||
prev_ts = 0
|
prev_ts = 0
|
||||||
prev_key = (prev.get('src_ip',''), prev.get('dest_ip',''),
|
prev_key = (
|
||||||
prev.get('dest_port',''), prev.get('ip.protocol',0))
|
prev.get('src_ip', ''),
|
||||||
if key == prev_key and (ts - prev_ts) < 60:
|
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
|
counts[-1] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
deduped.append(e)
|
deduped.append(e)
|
||||||
counts.append(1)
|
counts.append(1)
|
||||||
|
|
||||||
grouped = []
|
limit = int(limit) if limit else 50
|
||||||
group_counts = []
|
for e, count in list(zip(deduped, counts))[-limit:]:
|
||||||
for e in deduped:
|
|
||||||
ts_str = e.get('timestamp', '')
|
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', '')
|
src = e.get('src_ip', '')
|
||||||
dst = e.get('dest_ip', '')
|
dst = e.get('dest_ip', '')
|
||||||
port = e.get('dest_port', '')
|
port = str(e.get('dest_port', ''))
|
||||||
proto = e.get('ip.protocol', 0)
|
proto_num = int(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)
|
|
||||||
proto = proto_map.get(proto_num, str(proto_num))
|
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)
|
client = ip_to_name.get(src, src)
|
||||||
if filter_type and not client.startswith(filter_type + '-'):
|
svc_name = reverse_lookup(dst, port, proto)
|
||||||
continue
|
|
||||||
count_str = f" (x{count})" if count > 1 else ""
|
try:
|
||||||
print(f"{ts}|{client}|{dst_str}|{proto}{count_str}")
|
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):
|
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 = []
|
events = []
|
||||||
try:
|
try:
|
||||||
with open(file) as f:
|
with open(file) as f:
|
||||||
|
|
@ -306,9 +316,9 @@ def wg_events(file, filter_client, filter_type, limit):
|
||||||
if filter_type and not client.startswith(filter_type + '-'):
|
if filter_type and not client.startswith(filter_type + '-'):
|
||||||
continue
|
continue
|
||||||
events.append(e)
|
events.append(e)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Dedup consecutive same client+event+endpoint within 60s
|
# Dedup consecutive same client+event+endpoint within 60s
|
||||||
|
|
@ -317,43 +327,46 @@ def wg_events(file, filter_client, filter_type, limit):
|
||||||
for e in events:
|
for e in events:
|
||||||
ts_str = e.get('timestamp', '')
|
ts_str = e.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
|
||||||
ts = datetime.fromisoformat(ts_str).timestamp()
|
ts = datetime.fromisoformat(ts_str).timestamp()
|
||||||
except:
|
except Exception:
|
||||||
ts = 0
|
ts = 0
|
||||||
client = e.get('client', '')
|
client = e.get('client', '')
|
||||||
event = e.get('event', '')
|
event = e.get('event', '')
|
||||||
endpoint = e.get('endpoint', '')
|
endpoint = e.get('endpoint', '')
|
||||||
key = (client, event, endpoint[:15])
|
key = (client, event, endpoint[:15])
|
||||||
|
|
||||||
if deduped and counts:
|
if deduped:
|
||||||
prev = deduped[-1]
|
prev = deduped[-1]
|
||||||
prev_ts_str = prev.get('timestamp', '')
|
prev_ts_str = prev.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
prev_ts = datetime.fromisoformat(prev_ts_str).timestamp()
|
prev_ts = datetime.fromisoformat(prev_ts_str).timestamp()
|
||||||
except:
|
except Exception:
|
||||||
prev_ts = 0
|
prev_ts = 0
|
||||||
prev_key = (prev.get('client',''), prev.get('event',''), prev.get('endpoint','')[:15])
|
prev_key = (
|
||||||
if key == prev_key and (ts - prev_ts) < 60:
|
prev.get('client', ''),
|
||||||
|
prev.get('event', ''),
|
||||||
|
prev.get('endpoint', '')[:15]
|
||||||
|
)
|
||||||
|
if key == prev_key and (ts - prev_ts) < 300:
|
||||||
counts[-1] += 1
|
counts[-1] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
deduped.append(e)
|
deduped.append(e)
|
||||||
counts.append(1)
|
counts.append(1)
|
||||||
|
|
||||||
for e, count in zip(deduped[-int(limit):], counts[-int(limit):]):
|
limit = int(limit) if limit else 50
|
||||||
ts = e.get('timestamp', '')
|
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:
|
try:
|
||||||
from datetime import datetime
|
dt = datetime.fromisoformat(ts_str)
|
||||||
dt = datetime.fromisoformat(ts)
|
ts_fmt = dt.strftime('%d/%m %H:%M')
|
||||||
ts = dt.strftime(DATETIME_FMT)
|
except Exception:
|
||||||
except:
|
ts_fmt = ts_str
|
||||||
pass
|
|
||||||
client = e.get('client', '—')
|
print(f"{ts_fmt}|{client}|{endpoint}|{event}|{count}")
|
||||||
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}")
|
|
||||||
|
|
||||||
def format_fw_event(line, clients_dir):
|
def format_fw_event(line, clients_dir):
|
||||||
"""Format a single fw_event line"""
|
"""Format a single fw_event line"""
|
||||||
|
|
@ -2448,7 +2461,7 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def reverse_lookup(dest_ip, dest_port, proto):
|
def reverse_lookup(dest_ip, dest_port, proto):
|
||||||
for svc_name, svc in net_data.items():
|
for svc_name, svc in net_data.items():
|
||||||
if not isinstance(svc, dict):
|
if not isinstance(svc, dict):
|
||||||
continue
|
continue
|
||||||
|
|
@ -2459,14 +2472,105 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||||
for port_name, port_def in ports.items():
|
for port_name, port_def in ports.items():
|
||||||
if not isinstance(port_def, dict):
|
if not isinstance(port_def, dict):
|
||||||
continue
|
continue
|
||||||
if (str(port_def.get('port', '')) == dest_port and
|
if (str(port_def.get('port', '')) == str(dest_port) and
|
||||||
port_def.get('proto', 'tcp') == proto):
|
port_def.get('proto', 'tcp') == proto):
|
||||||
return f"{svc_name}:{port_name}"
|
return f"{svc_name}:{port_name}"
|
||||||
return svc_name
|
return svc_name
|
||||||
else:
|
|
||||||
return svc_name
|
return svc_name
|
||||||
return ''
|
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 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):
|
def make_dest_display(dest_ip, dest_port, proto, svc_name):
|
||||||
if svc_name:
|
if svc_name:
|
||||||
return svc_name
|
return svc_name
|
||||||
|
|
@ -2563,8 +2667,8 @@ commands = {
|
||||||
'filter_values': lambda args: filter_values(args[0], args[1], args[2]),
|
'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]),
|
'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]),
|
'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]),
|
'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]),
|
'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_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]),
|
||||||
'format_wg_event': lambda args: format_wg_event(sys.stdin.read()),
|
'format_wg_event': lambda args: format_wg_event(sys.stdin.read()),
|
||||||
'remove_events': lambda args: remove_events(args[0], args[1]),
|
'remove_events': lambda args: remove_events(args[0], args[1]),
|
||||||
|
|
|
||||||
|
|
@ -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_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}"
|
||||||
declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}"
|
declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}"
|
||||||
|
|
||||||
|
|
||||||
function config::_init_defaults() {
|
function config::_init_defaults() {
|
||||||
_WG_INTERFACE="${WG_INTERFACE:-wg0}"
|
_WG_INTERFACE="${WG_INTERFACE:-wg0}"
|
||||||
_WG_DNS="${WG_DNS:-10.0.0.103}"
|
_WG_DNS="${WG_DNS:-10.0.0.103}"
|
||||||
|
|
|
||||||
167
modules/ui/logs.module.sh
Normal file
167
modules/ui/logs.module.sh
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue