Compare commits
3 commits
7aff1d146d
...
1cfa5528c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cfa5528c8 | ||
|
|
4dcf98b128 | ||
|
|
57e08e88c4 |
12 changed files with 1090 additions and 824 deletions
|
|
@ -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 <n> 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 <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
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ function cmd::rule::on_load() {
|
|||
flag::register --peer
|
||||
flag::register --peers
|
||||
flag::register --dns-redirect
|
||||
flag::register --color
|
||||
flag::register --base
|
||||
flag::register --no-base
|
||||
flag::register --tree
|
||||
flag::register --detailed
|
||||
flag::register --resolved
|
||||
flag::register --force
|
||||
flag::register --type
|
||||
|
|
@ -60,7 +60,7 @@ Options for list:
|
|||
--base Show only base rules
|
||||
--no-base Hide base rules section
|
||||
--group <name> Filter by group (case insensitive)
|
||||
--tree Show full inheritance tree inline
|
||||
--detailed Show rule entries inline
|
||||
|
||||
Options for add:
|
||||
--name <name> Rule name
|
||||
|
|
@ -72,8 +72,8 @@ Options for add:
|
|||
--allow-port <ip:port:proto> Allow specific port (repeatable)
|
||||
--block-ip <ip/cidr> Block IP or subnet (repeatable)
|
||||
--block-port <ip:port:proto> Block specific port (repeatable)
|
||||
--block-service <name> Block named service — resolved to IP/port at creation (repeatable)
|
||||
--allow-service <name> Allow named service — resolved to IP/port at creation (repeatable)
|
||||
--block-service <name> Block named service (repeatable)
|
||||
--allow-service <name> Allow named service (repeatable)
|
||||
--dns-redirect Force DNS through Pi-hole
|
||||
|
||||
Options for update:
|
||||
|
|
@ -85,7 +85,7 @@ Options for update:
|
|||
--remove-block-ip <ip> Remove block IP entry
|
||||
--remove-block-port <entry> Remove block port entry
|
||||
|
||||
Options for show/inspect:
|
||||
Options for show:
|
||||
--name <name> Rule name
|
||||
--resolved Show resolved/merged entries
|
||||
--no-peers Hide assigned peers
|
||||
|
|
@ -96,14 +96,12 @@ Options for reapply:
|
|||
|
||||
Examples:
|
||||
wgctl rule list
|
||||
wgctl rule list --tree
|
||||
wgctl rule list --detailed
|
||||
wgctl rule list --group "VM Rules"
|
||||
wgctl rule show --name guest
|
||||
wgctl rule show --name moonlight-02 --resolved
|
||||
wgctl rule add --name no-proxmox --base --block-service proxmox
|
||||
wgctl rule add --name dev-01 --desc "Dev access" --group "Dev" --extends no-lan
|
||||
wgctl rule add --name restricted-dns --allow-service pihole:dns --block-service pihole
|
||||
wgctl rule update --name user --add-extends no-nginx
|
||||
wgctl rule add --name dev-01 --desc "Dev access" --extends no-lan
|
||||
wgctl rule assign --name dev-01 --peer laptop-nuno
|
||||
wgctl rule reapply --all
|
||||
EOF
|
||||
|
|
@ -118,17 +116,16 @@ function cmd::rule::run() {
|
|||
shift || true
|
||||
|
||||
case "$subcmd" in
|
||||
list|ls) cmd::rule::list "$@" ;;
|
||||
show|inspect) cmd::rule::show "$@" ;;
|
||||
inspect) cmd::rule::inspect "$@" ;;
|
||||
add|new|create) cmd::rule::add "$@" ;;
|
||||
update|edit) cmd::rule::update "$@" ;;
|
||||
remove|rm|del|delete) cmd::rule::remove "$@" ;;
|
||||
assign) cmd::rule::assign "$@" ;;
|
||||
list|ls) cmd::rule::list "$@" ;;
|
||||
show|inspect) cmd::rule::show "$@" ;;
|
||||
add|new|create) cmd::rule::add "$@" ;;
|
||||
update|edit) cmd::rule::update "$@" ;;
|
||||
remove|rm|del|delete) cmd::rule::remove "$@" ;;
|
||||
assign) cmd::rule::assign "$@" ;;
|
||||
unassign) cmd::rule::unassign "$@" ;;
|
||||
migrate) cmd::rule::migrate "$@" ;;
|
||||
reapply) cmd::rule::reapply "$@" ;;
|
||||
help) cmd::rule::help ;;
|
||||
migrate) cmd::rule::migrate "$@" ;;
|
||||
reapply) cmd::rule::reapply "$@" ;;
|
||||
help) cmd::rule::help ;;
|
||||
*)
|
||||
log::error "Unknown subcommand: '${subcmd}'"
|
||||
cmd::rule::help
|
||||
|
|
@ -141,172 +138,98 @@ function cmd::rule::run() {
|
|||
# List
|
||||
# ============================================
|
||||
|
||||
function cmd::rule::_pad() {
|
||||
local text="$1" width="$2"
|
||||
local visible
|
||||
visible=$(printf "%s" "$text" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
local visible_len=${#visible}
|
||||
local byte_len=${#text}
|
||||
local extra=$(( byte_len - visible_len ))
|
||||
printf "%-$(( width + extra ))s" "$text"
|
||||
}
|
||||
|
||||
function cmd::rule::_print_extends_tree() {
|
||||
local extends="$1" indent="${2:-2}" rules_dir="$3"
|
||||
[[ -z "$extends" ]] && return 0
|
||||
|
||||
local extend_list=()
|
||||
IFS=',' read -ra extend_list <<< "$extends"
|
||||
|
||||
for base in "${extend_list[@]}"; do
|
||||
[[ -z "$base" ]] && continue
|
||||
local spaces
|
||||
spaces=$(printf '%*s' "$indent" '')
|
||||
printf " \033[0;37m%s↳ %s\033[0m\n" "$spaces" "$base"
|
||||
|
||||
if [[ "$indent" -lt 12 ]]; then
|
||||
local sub_file=""
|
||||
if sub_file=$(json::find_rule_file "$rules_dir" "$base" 2>/dev/null); then
|
||||
local sub_extends=""
|
||||
sub_extends=$(json::get "$sub_file" "extends" 2>/dev/null \
|
||||
| tr '\n' ',' | sed 's/,$//' || true)
|
||||
if [[ -n "$sub_extends" ]]; then
|
||||
cmd::rule::_print_extends_tree \
|
||||
"$sub_extends" $(( indent + 4 )) "$rules_dir"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function cmd::rule::list() {
|
||||
local rules_dir
|
||||
rules_dir="$(ctx::rules)"
|
||||
|
||||
local show_base_only=false
|
||||
local show_base=true
|
||||
local filter_group=""
|
||||
local show_tree=false
|
||||
local found_any=false
|
||||
local show_base_only=false show_base=true
|
||||
local filter_group="" detailed=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--base) show_base_only=true; shift ;;
|
||||
--no-base) show_base=false; shift ;;
|
||||
--group) util::require_flag "--group" "${2:-}" || return 1
|
||||
filter_group="${2,,}"; shift 2 ;;
|
||||
--tree) show_tree=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
--base) show_base_only=true; shift ;;
|
||||
--no-base) show_base=false; shift ;;
|
||||
--group) filter_group="${2,,}"; shift 2 ;;
|
||||
--detailed) detailed=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
local rules=("${rules_dir}"/*.rule)
|
||||
if [[ ! -f "${rules[0]}" ]]; then
|
||||
log::wg "No rules configured"
|
||||
return 0
|
||||
fi
|
||||
local data
|
||||
data=$(json::rule_list_data "$rules_dir" "$(ctx::meta)")
|
||||
[[ -z "$data" ]] && log::wg "No rules configured" && return 0
|
||||
|
||||
local header_printed=false
|
||||
local printing_base=false
|
||||
local current_group=""
|
||||
# Measure max name width
|
||||
local w_name=12
|
||||
while IFS='|' read -r name rest; do
|
||||
[[ -z "$name" ]] && continue
|
||||
(( ${#name} > w_name )) && w_name=${#name}
|
||||
done <<< "$data"
|
||||
(( w_name += 2 ))
|
||||
|
||||
log::section "Firewall Rules"
|
||||
echo ""
|
||||
|
||||
local current_group="" printing_base=false found_any=false
|
||||
|
||||
while IFS="|" read -r name desc n_allows n_blocks \
|
||||
peer_count extends is_base group; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
# --base: show ONLY base rules
|
||||
if $show_base_only && [[ "$is_base" == "False" ]]; then
|
||||
continue
|
||||
fi
|
||||
$show_base_only && [[ "$is_base" == "False" ]] && continue
|
||||
! $show_base && [[ "$is_base" == "True" ]] && continue
|
||||
[[ -n "$filter_group" && "${group,,}" != "$filter_group" ]] && continue
|
||||
|
||||
# --no-base: hide base rules
|
||||
if ! $show_base && [[ "$is_base" == "True" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# --group filter (case insensitive)
|
||||
if [[ -n "$filter_group" && "${group,,}" != "$filter_group" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Print header on first match
|
||||
if ! $header_printed; then
|
||||
log::section "Firewall Rules"
|
||||
printf "\n %-20s %-40s %-8s %-8s %s\n" \
|
||||
"NAME" "DESCRIPTION" \
|
||||
"$(ui::center "ALLOWS" 8)" \
|
||||
"$(ui::center "BLOCKS" 8)" \
|
||||
"PEERS"
|
||||
local divider
|
||||
divider=$(printf '─%.0s' {1..88})
|
||||
printf " %s\n" "$divider"
|
||||
header_printed=true
|
||||
fi
|
||||
found_any=true
|
||||
|
||||
# Base rules section header
|
||||
if [[ "$is_base" == "True" && "$printing_base" == "false" ]]; then
|
||||
if ! $show_base_only; then
|
||||
local bdashes
|
||||
bdashes=$(printf '─%.0s' {1..74})
|
||||
printf "\n \033[0;37m── Base Rules \033[0m%s\n" "$bdashes"
|
||||
ui::rule::list_base_header
|
||||
fi
|
||||
printing_base=true
|
||||
current_group=""
|
||||
fi
|
||||
|
||||
# Group header — only for non-base rules
|
||||
# Group header — non-base rules only
|
||||
if [[ "$is_base" == "False" && "${group,,}" != "${current_group,,}" ]]; then
|
||||
if [[ -n "$group" ]]; then
|
||||
printf "\n \033[0;36m▸ %s\033[0m\n" "$group"
|
||||
ui::rule::list_group_header "$group"
|
||||
elif [[ -n "$current_group" ]]; then
|
||||
printf "\n"
|
||||
echo ""
|
||||
fi
|
||||
current_group="$group"
|
||||
fi
|
||||
|
||||
local short_desc="${desc:0:35}"
|
||||
[[ ${#desc} -gt 35 ]] && short_desc="${short_desc}..."
|
||||
# Rule row
|
||||
# ui::rule::list_row "$name" "$n_allows" "$n_blocks" "$peer_count" "$w_name"
|
||||
|
||||
local desc_col_width=40
|
||||
[[ "${short_desc:-—}" == "—" ]] && desc_col_width=42
|
||||
# Extends
|
||||
# Rule row — pass extends_csv for compact inline display
|
||||
local compact_extends=""
|
||||
if [[ -z "$detailed" ]] || ! $detailed; then
|
||||
compact_extends="$extends"
|
||||
fi
|
||||
ui::rule::list_row "$name" "$n_allows" "$n_blocks" "$peer_count" "$w_name" "$compact_extends"
|
||||
|
||||
found_any=true
|
||||
|
||||
printf " %-20s %-${desc_col_width}s %-8s %-8s %s\n" \
|
||||
"$name" "${short_desc:-—}" \
|
||||
"$(ui::center "$n_allows" 8)" \
|
||||
"$(ui::center "$n_blocks" 8)" \
|
||||
"${peer_count} peers"
|
||||
|
||||
# Print extends
|
||||
if [[ -n "$extends" ]]; then
|
||||
if $show_tree; then
|
||||
cmd::rule::_print_extends_tree "$extends" 2 "$rules_dir"
|
||||
else
|
||||
local extend_list=()
|
||||
IFS=',' read -ra extend_list <<< "$extends"
|
||||
for base in "${extend_list[@]}"; do
|
||||
[[ -z "$base" ]] && continue
|
||||
printf " \033[0;37m ↳ %s\033[0m\n" "$base"
|
||||
done
|
||||
fi
|
||||
printf "\n"
|
||||
# Detailed mode — show expanded entries
|
||||
if $detailed && [[ -n "$extends" ]]; then
|
||||
ui::rule::list_extends_detailed "$extends" "$rules_dir"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
done < <(json::rule_list_data "$rules_dir" "$(ctx::meta)")
|
||||
done <<< "$data"
|
||||
|
||||
if ! $found_any; then
|
||||
if [[ -n "$filter_group" ]]; then
|
||||
log::wg_warning "No rules found in group: ${filter_group}"
|
||||
else
|
||||
$found_any || {
|
||||
[[ -n "$filter_group" ]] && \
|
||||
log::wg_warning "No rules found in group: ${filter_group}" || \
|
||||
log::wg_warning "No rules found"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
printf "\n"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -318,11 +241,10 @@ function cmd::rule::show() {
|
|||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) util::require_flag "--name" "${2:-}" || return 1
|
||||
name="$2"; shift 2 ;;
|
||||
--no-peers) show_peers=false; shift ;;
|
||||
--resolved) show_resolved=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--no-peers) show_peers=false; shift ;;
|
||||
--resolved) show_resolved=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -333,7 +255,7 @@ function cmd::rule::show() {
|
|||
local rule_file
|
||||
rule_file="$(rule::path "$name")"
|
||||
|
||||
# ── DNS display ───────────────────────────────
|
||||
# DNS display
|
||||
local dns_redirect resolved_dns dns_display
|
||||
dns_redirect=$(rule::get_own "$name" "dns_redirect")
|
||||
dns_redirect="${dns_redirect:-false}"
|
||||
|
|
@ -359,24 +281,21 @@ function cmd::rule::show() {
|
|||
ui::row "DNS" "$dns_display"
|
||||
|
||||
printf "\n"
|
||||
# ── Extends + own rules ────────────────────────
|
||||
if rule::render_extends_tree "$name"; then
|
||||
# Has inheritance — tree already rendered
|
||||
if ui::rule::tree "$name"; then
|
||||
:
|
||||
else
|
||||
# No inheritance — flat view
|
||||
rule::render_flat "$name"
|
||||
ui::rule::flat "$name"
|
||||
printf "\n"
|
||||
fi
|
||||
|
||||
# ── Resolved ──────────────────────────────────
|
||||
# Resolved view
|
||||
if $show_resolved; then
|
||||
cmd::rule::_show_section "Resolved (applied to peers)"
|
||||
ui::rule::section_header "Resolved (applied to peers)"
|
||||
printf "\n"
|
||||
local res_allow_ports res_allow_ips res_block_ips res_block_ports
|
||||
res_allow_ports=$(rule::get "$name" "allow_ports")
|
||||
res_allow_ips=$(rule::get "$name" "allow_ips")
|
||||
res_block_ips=$(rule::get "$name" "block_ips")
|
||||
res_allow_ips=$(rule::get "$name" "allow_ips")
|
||||
res_block_ips=$(rule::get "$name" "block_ips")
|
||||
res_block_ports=$(rule::get "$name" "block_ports")
|
||||
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "+" "$e"; done \
|
||||
<<< "$res_allow_ports"$'\n'"$res_allow_ips"
|
||||
|
|
@ -385,26 +304,24 @@ function cmd::rule::show() {
|
|||
printf "\n"
|
||||
fi
|
||||
|
||||
# ── Peers ─────────────────────────────────────
|
||||
# Peers
|
||||
$show_peers || return 0
|
||||
|
||||
local peer_list=()
|
||||
mapfile -t peer_list < <(peers::with_rule "$name")
|
||||
|
||||
mapfile -t peer_list < <(peers::with_rule "$name") || true
|
||||
local peer_count=${#peer_list[@]}
|
||||
|
||||
ui::empty "$peer_count" && return 0
|
||||
|
||||
printf "\n"
|
||||
printf "\033[0;37m── Peers (%s) \033[0m%s\n\n" \
|
||||
"$(color::gray "${peer_count}")" \
|
||||
"$(printf '\033[0;37m─%.0s' {1..35})"
|
||||
local peer_word="peers"
|
||||
[[ "$peer_count" -eq 1 ]] && peer_word="peer"
|
||||
printf "\n \033[0;37m── Peers (%s %s) \033[0m%s\n\n" \
|
||||
"$peer_count" "$peer_word" "$(printf '\033[0;37m─%.0s' {1..30})"
|
||||
|
||||
if $show_peers && [[ $peer_count -gt 0 ]]; then
|
||||
for peer_name in "${peer_list[@]}"; do
|
||||
local ip
|
||||
ip=$(peers::get_ip "$peer_name")
|
||||
printf " %-28s %s\n" "$peer_name" "$ip"
|
||||
done
|
||||
fi
|
||||
for peer_name in "${peer_list[@]}"; do
|
||||
local ip
|
||||
ip=$(peers::get_ip "$peer_name")
|
||||
printf " %-28s %s\n" "$peer_name" "$ip"
|
||||
done
|
||||
printf "\n"
|
||||
return 0
|
||||
}
|
||||
|
|
@ -418,27 +335,26 @@ function cmd::rule::add() {
|
|||
local extends=()
|
||||
local allow_ips=() block_ips=() block_ports=() allow_ports=()
|
||||
local block_services=() allow_services=()
|
||||
local dns_redirect=false
|
||||
local is_base=false
|
||||
local dns_redirect=false is_base=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--desc) desc="$2"; shift 2 ;;
|
||||
--group) group="$2"; shift 2 ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--desc) desc="$2"; shift 2 ;;
|
||||
--group) group="$2"; shift 2 ;;
|
||||
--extends)
|
||||
IFS=',' read -ra ext <<< "$2"
|
||||
extends+=("${ext[@]}")
|
||||
shift 2 ;;
|
||||
--base) is_base=true; shift ;;
|
||||
--allow-ip) allow_ips+=("$2"); shift 2 ;;
|
||||
--allow-port) allow_ports+=("$2"); shift 2 ;;
|
||||
--block-ip) block_ips+=("$2"); shift 2 ;;
|
||||
--block-port) block_ports+=("$2"); shift 2 ;;
|
||||
--block-service) block_services+=("$2"); shift 2 ;;
|
||||
--allow-service) allow_services+=("$2"); shift 2 ;;
|
||||
--dns-redirect) dns_redirect=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
--base) is_base=true; shift ;;
|
||||
--allow-ip) allow_ips+=("$2"); shift 2 ;;
|
||||
--allow-port) allow_ports+=("$2"); shift 2 ;;
|
||||
--block-ip) block_ips+=("$2"); shift 2 ;;
|
||||
--block-port) block_ports+=("$2"); shift 2 ;;
|
||||
--block-service) block_services+=("$2"); shift 2 ;;
|
||||
--allow-service) allow_services+=("$2"); shift 2 ;;
|
||||
--dns-redirect) dns_redirect=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
return 1 ;;
|
||||
|
|
@ -452,12 +368,10 @@ function cmd::rule::add() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Validate extends
|
||||
for ext in "${extends[@]}"; do
|
||||
rule::require_exists "$ext" || return 1
|
||||
done
|
||||
|
||||
# Determine target directory
|
||||
local rule_dir
|
||||
if $is_base; then
|
||||
rule_dir="$(ctx::rules)/base"
|
||||
|
|
@ -487,7 +401,6 @@ function cmd::rule::add() {
|
|||
done
|
||||
|
||||
local rule_file="${rule_dir}/${name}.rule"
|
||||
|
||||
local allow_str block_str port_str allow_port_str extends_str
|
||||
allow_str=$(IFS=','; echo "${allow_ips[*]}")
|
||||
block_str=$(IFS=','; echo "${block_ips[*]}")
|
||||
|
|
@ -502,7 +415,6 @@ function cmd::rule::add() {
|
|||
|
||||
local base_label=""
|
||||
$is_base && base_label=" (base)"
|
||||
|
||||
log::wg_success "Rule created: ${name}${base_label}"
|
||||
}
|
||||
|
||||
|
|
@ -519,9 +431,9 @@ function cmd::rule::update() {
|
|||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--desc) desc="$2"; shift 2 ;;
|
||||
--group) group="$2"; shift 2 ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--desc) desc="$2"; shift 2 ;;
|
||||
--group) group="$2"; shift 2 ;;
|
||||
--add-extends)
|
||||
IFS=',' read -ra ext <<< "$2"
|
||||
add_extends+=("${ext[@]}")
|
||||
|
|
@ -530,16 +442,16 @@ function cmd::rule::update() {
|
|||
IFS=',' read -ra ext <<< "$2"
|
||||
rm_extends+=("${ext[@]}")
|
||||
shift 2 ;;
|
||||
--allow-ip) allow_ips+=("$2"); shift 2 ;;
|
||||
--allow-port) allow_ports+=("$2"); shift 2 ;;
|
||||
--block-ip) block_ips+=("$2"); shift 2 ;;
|
||||
--block-port) block_ports+=("$2"); shift 2 ;;
|
||||
--remove-allow-ip) rm_allow_ips+=("$2"); shift 2 ;;
|
||||
--remove-block-ip) rm_block_ips+=("$2"); shift 2 ;;
|
||||
--remove-block-port) rm_block_ports+=("$2"); shift 2 ;;
|
||||
--remove-allow-port) rm_allow_ports+=("$2"); shift 2 ;;
|
||||
--dns-redirect) dns_redirect=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
--allow-ip) allow_ips+=("$2"); shift 2 ;;
|
||||
--allow-port) allow_ports+=("$2"); shift 2 ;;
|
||||
--block-ip) block_ips+=("$2"); shift 2 ;;
|
||||
--block-port) block_ports+=("$2"); shift 2 ;;
|
||||
--remove-allow-ip) rm_allow_ips+=("$2"); shift 2 ;;
|
||||
--remove-block-ip) rm_block_ips+=("$2"); shift 2 ;;
|
||||
--remove-block-port) rm_block_ports+=("$2"); shift 2 ;;
|
||||
--remove-allow-port) rm_allow_ports+=("$2"); shift 2 ;;
|
||||
--dns-redirect) dns_redirect=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
return 1 ;;
|
||||
|
|
@ -552,18 +464,15 @@ function cmd::rule::update() {
|
|||
local rule_file
|
||||
rule_file="$(rule::path "$name")"
|
||||
|
||||
# Update simple fields
|
||||
[[ -n "$desc" ]] && json::set "$rule_file" "desc" "\"$desc\""
|
||||
[[ -n "$group" ]] && json::set "$rule_file" "group" "\"$group\""
|
||||
[[ -n "$dns_redirect" ]] && json::set "$rule_file" "dns_redirect" "true"
|
||||
|
||||
# Add entries
|
||||
for ip in "${allow_ips[@]}"; do json::append "$rule_file" "allow_ips" "$ip"; done
|
||||
for ip in "${block_ips[@]}"; do json::append "$rule_file" "block_ips" "$ip"; done
|
||||
for p in "${block_ports[@]}"; do json::append "$rule_file" "block_ports" "$p"; done
|
||||
for p in "${allow_ports[@]}"; do json::append "$rule_file" "allow_ports" "$p"; done
|
||||
|
||||
# Add/remove extends
|
||||
for ext in "${add_extends[@]}"; do
|
||||
rule::require_exists "$ext" || return 1
|
||||
json::append "$rule_file" "extends" "$ext"
|
||||
|
|
@ -572,7 +481,6 @@ function cmd::rule::update() {
|
|||
json::remove "$rule_file" "extends" "$ext"
|
||||
done
|
||||
|
||||
# Remove entries
|
||||
for ip in "${rm_allow_ips[@]}"; do json::remove "$rule_file" "allow_ips" "$ip"; done
|
||||
for ip in "${rm_block_ips[@]}"; do json::remove "$rule_file" "block_ips" "$ip"; done
|
||||
for p in "${rm_block_ports[@]}"; do json::remove "$rule_file" "block_ports" "$p"; done
|
||||
|
|
@ -583,8 +491,7 @@ function cmd::rule::update() {
|
|||
}
|
||||
|
||||
# ============================================
|
||||
# Remove / Assign / Unassign / Migrate / Reapply
|
||||
# (unchanged from original)
|
||||
# Remove
|
||||
# ============================================
|
||||
|
||||
function cmd::rule::remove() {
|
||||
|
|
@ -603,7 +510,7 @@ function cmd::rule::remove() {
|
|||
rule::require_exists "$name" || return 1
|
||||
|
||||
local peer_list=()
|
||||
mapfile -t peer_list < <(peers::with_rule "$name")
|
||||
mapfile -t peer_list < <(peers::with_rule "$name") || true
|
||||
local peer_count=${#peer_list[@]}
|
||||
|
||||
if [[ "$peer_count" -gt 0 ]]; then
|
||||
|
|
@ -620,6 +527,10 @@ function cmd::rule::remove() {
|
|||
log::wg_success "Rule removed: ${name}"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Assign / Unassign
|
||||
# ============================================
|
||||
|
||||
function cmd::rule::assign() {
|
||||
local name="" peer="" type=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
|
@ -635,15 +546,13 @@ function cmd::rule::assign() {
|
|||
[[ -z "$name" || -z "$peer" ]] && \
|
||||
log::error "Missing required flags: --name and --peer" && return 1
|
||||
|
||||
rule::require_exists "$name" || return 1
|
||||
rule::require_exists "$name" || return 1
|
||||
rule::require_assignable "$name" || return 1
|
||||
|
||||
peer=$(peers::resolve_and_require "$peer" "$type") || return 1
|
||||
|
||||
local existing_rule
|
||||
local existing_rule ip
|
||||
existing_rule=$(peers::get_meta "$peer" "rule")
|
||||
|
||||
local ip
|
||||
ip=$(peers::get_ip "$peer")
|
||||
[[ -z "$ip" ]] && log::error "Could not resolve IP for: $peer" && return 1
|
||||
|
||||
|
|
@ -684,37 +593,41 @@ function cmd::rule::unassign() {
|
|||
log::wg_success "Unassigned rule from: ${peer}"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Migrate
|
||||
# ============================================
|
||||
|
||||
function cmd::rule::migrate() {
|
||||
log::section "Migrating peers to default rules"
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
local count=0
|
||||
|
||||
while IFS= read -r peer_name; do
|
||||
local existing
|
||||
existing=$(peers::get_meta "$peer_name" "rule")
|
||||
[[ -n "$existing" ]] && continue
|
||||
local default_rule
|
||||
default_rule=$(peers::default_rule "$peer_name")
|
||||
|
||||
# Try to get default rule from subnet policy
|
||||
local peer_type subnet_name default_rule
|
||||
peer_type=$(peers::get_meta "$peer_name" "type")
|
||||
subnet_name=$(peers::get_meta "$peer_name" "subnet")
|
||||
default_rule=$(subnet::default_rule "$subnet_name" "$peer_type")
|
||||
[[ -z "$default_rule" ]] && continue
|
||||
|
||||
rule::exists "$default_rule" || continue
|
||||
|
||||
local ip
|
||||
ip=$(peers::get_ip "$peer_name")
|
||||
echo "${peer_name} ${default_rule} ${ip}" >> "$tmp"
|
||||
done < <(peers::all)
|
||||
|
||||
local count=0
|
||||
local lines=()
|
||||
mapfile -t lines < "$tmp"
|
||||
rm -f "$tmp"
|
||||
|
||||
for line in "${lines[@]}"; do
|
||||
IFS=" " read -r peer_name default_rule ip <<< "$line"
|
||||
rule::apply "$default_rule" "$ip" "$peer_name" </dev/null
|
||||
rule::apply "$default_rule" "$ip" "$peer_name"
|
||||
(( count++ )) || true
|
||||
done
|
||||
done < <(peers::all)
|
||||
|
||||
log::wg_success "Migrated ${count} peers"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Reapply
|
||||
# ============================================
|
||||
|
||||
function cmd::rule::reapply() {
|
||||
local name="" all=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
|
@ -731,9 +644,8 @@ function cmd::rule::reapply() {
|
|||
while IFS= read -r rule_file; do
|
||||
local rname
|
||||
rname=$(basename "$rule_file" .rule)
|
||||
# Skip if no peers assigned
|
||||
local peer_list=()
|
||||
mapfile -t peer_list < <(peers::with_rule "$rname")
|
||||
mapfile -t peer_list < <(peers::with_rule "$rname") || true
|
||||
[[ ${#peer_list[@]} -eq 0 ]] && continue
|
||||
rule::reapply_all "$rname"
|
||||
(( count++ )) || true
|
||||
|
|
@ -747,19 +659,3 @@ function cmd::rule::reapply() {
|
|||
rule::reapply_all "$name"
|
||||
log::wg_success "Rule '${name}' reapplied"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Show helpers
|
||||
# ============================================
|
||||
|
||||
function cmd::rule::_show_section() {
|
||||
local title="${1:-}" color="${2:-white}" use_color="${3:-false}"
|
||||
local color_code=""
|
||||
if $use_color; then
|
||||
case "$color" in
|
||||
green) color_code="\033[0;32m" ;;
|
||||
red) color_code="\033[0;31m" ;;
|
||||
esac
|
||||
fi
|
||||
printf "\n ${color_code}── %s ──────────────────────────────────\033[0m\n" "$title"
|
||||
}
|
||||
|
|
@ -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 <<EOF
|
||||
Usage: wgctl watch [options]
|
||||
|
||||
Live monitor of WireGuard activity. Shows:
|
||||
Live monitor of WireGuard activity.
|
||||
- Handshakes from connected peers (green)
|
||||
- Connection attempts from blocked peers (red, SOURCE: wg)
|
||||
- Firewall drops from rule-restricted peers (red, SOURCE: fw)
|
||||
- Connection attempts from blocked peers (red)
|
||||
- Firewall drops from restricted peers (red)
|
||||
|
||||
Options:
|
||||
--name <name> Filter by client name
|
||||
--type <type> Filter by device type
|
||||
--peers <list> 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::last_event() { python3 "$JSON_HELPER" last_event "$@" </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::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" wg_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_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_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
|
||||
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
|
||||
|
|
|
|||
|
|
@ -154,11 +154,16 @@ def events_for(file, ip, limit):
|
|||
except:
|
||||
pass
|
||||
|
||||
def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
||||
"""Format firewall drop events with dedup and counts"""
|
||||
def fw_events(file, filter_ip, filter_type, clients_dir, net_file, limit):
|
||||
"""
|
||||
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
|
||||
from datetime import datetime
|
||||
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
||||
|
||||
# Build ip->name map
|
||||
ip_to_name = {}
|
||||
for conf in glob.glob(f"{clients_dir}/*.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'):
|
||||
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 = {}
|
||||
|
||||
|
|
@ -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])
|
||||
key = (client, event, endpoint[:15])
|
||||
|
||||
if deduped and counts:
|
||||
prev = deduped[-1]
|
||||
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]),
|
||||
|
|
|
|||
23
core/ui.sh
23
core/ui.sh
|
|
@ -64,6 +64,29 @@ for s in sys.argv[1:]:
|
|||
" "$@"
|
||||
}
|
||||
|
||||
# ui::vis_len <string>
|
||||
# Returns the visible (character) length of a string,
|
||||
# stripping ANSI codes and accounting for multi-byte UTF-8.
|
||||
function ui::vis_len() {
|
||||
local str="${1:-}"
|
||||
# Strip ANSI codes first
|
||||
local clean
|
||||
clean=$(echo "$str" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
# Use Python for accurate Unicode character count
|
||||
python3 -c "import sys; print(len('${clean//\'/\'\\\'\'}'))" 2>/dev/null || echo "${#clean}"
|
||||
}
|
||||
|
||||
# ui::pad_to_col <string_bytes_printed> <target_visible_col>
|
||||
# Returns padding needed accounting for UTF-8 byte/char difference.
|
||||
# extra_bytes = bytes_printed - visible_chars_printed
|
||||
function ui::utf8_extra_bytes() {
|
||||
local str="${1:-}"
|
||||
local byte_len=${#str}
|
||||
local vis_len
|
||||
vis_len=$(ui::vis_len "$str")
|
||||
echo $(( byte_len - vis_len ))
|
||||
}
|
||||
|
||||
|
||||
function ui::pad_status() {
|
||||
ui::pad "${1:-}" "${2:-25}"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@
|
|||
"desktop-roboclean": "46.189.215.231",
|
||||
"laptop-nuno": "94.63.0.129",
|
||||
"phone-luis": "176.223.61.15",
|
||||
"phone-helena-2": "148.69.192.130",
|
||||
"phone-helena-2": "148.69.202.127",
|
||||
"desktop-zephyr": "86.120.152.74"
|
||||
}
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -333,21 +333,14 @@ function rule::restore_all() {
|
|||
# Rendering
|
||||
# ============================================
|
||||
|
||||
function rule::render_flat() {
|
||||
ui::rule::flat "$1"
|
||||
}
|
||||
# ======================================================
|
||||
# Aliases (for backward compat — remove in cleanup pass)
|
||||
# ======================================================
|
||||
|
||||
function rule::render_entries() {
|
||||
ui::rule::entries "$1"
|
||||
}
|
||||
|
||||
function rule::render_own_entries() {
|
||||
ui::rule::own_entries "$1"
|
||||
}
|
||||
|
||||
function rule::render_extends_tree() {
|
||||
ui::rule::tree "$1"
|
||||
}
|
||||
function rule::render_flat() { ui::rule::flat "$1"; }
|
||||
function rule::render_entries() { ui::rule::entries "$1"; }
|
||||
function rule::render_own_entries() { ui::rule::own_entries "$1"; }
|
||||
function rule::render_extends_tree() { ui::rule::tree "$1"; }
|
||||
|
||||
# ============================================
|
||||
# DNS Redirect
|
||||
|
|
|
|||
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"
|
||||
}
|
||||
|
|
@ -201,7 +201,6 @@ function ui::rule::_identity_rule_entry() {
|
|||
mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) || true
|
||||
|
||||
if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then
|
||||
# Rule has extends — render one level deep using shared helper
|
||||
ui::rule::_render_bases extends_raw 10 8
|
||||
|
||||
local own_output
|
||||
|
|
@ -211,7 +210,6 @@ function ui::rule::_identity_rule_entry() {
|
|||
printf "%s\n" "$own_output"
|
||||
fi
|
||||
else
|
||||
# Leaf rule — show own entries or note full access
|
||||
local own_output
|
||||
own_output=$(ui::rule::own_entries "$rule_name" 8)
|
||||
if [[ -n "$own_output" ]]; then
|
||||
|
|
@ -255,3 +253,102 @@ function ui::rule::_peer_rule_entry() {
|
|||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ======================================================
|
||||
# List Rendering
|
||||
# ======================================================
|
||||
|
||||
# ui::rule::list_group_header <group_name>
|
||||
function ui::rule::list_group_header() {
|
||||
local group="${1:-}"
|
||||
printf "\n \033[0;36m▸ %s\033[0m\n" "$group"
|
||||
}
|
||||
|
||||
# ui::rule::list_base_header
|
||||
function ui::rule::list_base_header() {
|
||||
printf "\n \033[2m── Base Rules ──────────────────────\033[0m\n"
|
||||
}
|
||||
|
||||
|
||||
# ui::rule::list_row <name> <n_allows> <n_blocks> <peer_count> <w_name>
|
||||
function ui::rule::list_row() {
|
||||
local name="${1:-}" n_allows="${2:-0}" n_blocks="${3:-0}" \
|
||||
peer_count="${4:-0}" w_name="${5:-16}" extends_csv="${6:-}"
|
||||
|
||||
local name_pad
|
||||
name_pad=$(printf "%-${w_name}s" "$name")
|
||||
|
||||
local peer_word="peers"
|
||||
[[ "$peer_count" -eq 1 ]] && peer_word="peer"
|
||||
|
||||
local peers_display
|
||||
peers_display=$(printf "%s %s" "$peer_count" "$peer_word")
|
||||
local peers_pad_n=$(( 10 - ${#peers_display} ))
|
||||
[[ $peers_pad_n -lt 0 ]] && peers_pad_n=0
|
||||
|
||||
local extends_indicator=""
|
||||
if [[ -n "$extends_csv" ]]; then
|
||||
local extends_display="${extends_csv//,/, }"
|
||||
extends_indicator=" \033[2m↳ ${extends_display}\033[0m"
|
||||
fi
|
||||
|
||||
# Build allows and blocks — pad to fixed visible width of 5
|
||||
local allows_str blocks_str
|
||||
if [[ "$n_allows" -eq 0 && "$n_blocks" -eq 0 ]]; then
|
||||
allows_str=$(ui::pad_mb "\033[1;32m+all\033[0m" 5)
|
||||
blocks_str=$(printf "%5s" "")
|
||||
else
|
||||
if [[ "$n_allows" -gt 0 ]]; then
|
||||
allows_str=$(ui::pad_mb "\033[1;32m+${n_allows}\033[0m" 5)
|
||||
else
|
||||
allows_str=$(printf "%5s" "")
|
||||
fi
|
||||
if [[ "$n_blocks" -gt 0 ]]; then
|
||||
blocks_str=$(ui::pad_mb "\033[1;31m-${n_blocks}\033[0m" 5)
|
||||
else
|
||||
blocks_str=$(printf "%5s" "")
|
||||
fi
|
||||
fi
|
||||
|
||||
printf " %s %b%b %s%*s%b\n" \
|
||||
"$name_pad" "$allows_str" "$blocks_str" \
|
||||
"$peers_display" "$peers_pad_n" "" "$extends_indicator"
|
||||
}
|
||||
|
||||
# ui::rule::list_extends <extends_csv>
|
||||
# Renders the extends tree for a rule in list view (compact, one level)
|
||||
function ui::rule::list_extends() {
|
||||
local extends_csv="${1:-}"
|
||||
[[ -z "$extends_csv" ]] && return 0
|
||||
|
||||
local extend_list=()
|
||||
IFS=',' read -ra extend_list <<< "$extends_csv"
|
||||
for base in "${extend_list[@]}"; do
|
||||
[[ -z "$base" ]] && continue
|
||||
printf " \033[0;37m ↳ %s\033[0m\n" "$base"
|
||||
done
|
||||
}
|
||||
|
||||
# ui::rule::list_extends_detailed <extends_csv> <rules_dir>
|
||||
# Renders the extends tree with entries expanded (--detailed mode)
|
||||
function ui::rule::list_extends_detailed() {
|
||||
local extends_csv="${1:-}" rules_dir="${2:-}"
|
||||
[[ -z "$extends_csv" ]] && return 0
|
||||
|
||||
local extend_list=()
|
||||
IFS=',' read -ra extend_list <<< "$extends_csv"
|
||||
for base in "${extend_list[@]}"; do
|
||||
[[ -z "$base" ]] && continue
|
||||
printf " \033[0;37m ↳ %s\033[0m\n" "$base"
|
||||
ui::rule::entries "$base" 6
|
||||
done
|
||||
}
|
||||
|
||||
# ======================================================
|
||||
# Show helpers
|
||||
# ======================================================
|
||||
|
||||
function ui::rule::section_header() {
|
||||
local title="${1:-}"
|
||||
printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue