feat: rule inheritance, rule groups, rule inspect, ui::center, fw dedup, activity metrics
This commit is contained in:
parent
a09c59a7c4
commit
6ac1a7d3a2
18 changed files with 2147 additions and 379 deletions
|
|
@ -205,9 +205,9 @@ function cmd::fw::_print_filtered() {
|
||||||
local show_nflog="$1" show_accept="$2" show_drop="$3"
|
local show_nflog="$1" show_accept="$2" show_drop="$3"
|
||||||
while IFS= read -r rule; do
|
while IFS= read -r rule; do
|
||||||
[[ -z "$rule" ]] && continue
|
[[ -z "$rule" ]] && continue
|
||||||
! $show_nflog && [[ "$rule" =~ NFLOG ]] && continue
|
if ! $show_nflog && [[ "$rule" =~ NFLOG ]]; then continue; fi
|
||||||
! $show_accept && [[ "$rule" =~ ACCEPT ]] && continue
|
if ! $show_accept && [[ "$rule" =~ ACCEPT ]]; then continue; fi
|
||||||
! $show_drop && [[ "$rule" =~ DROP ]] && continue
|
if ! $show_drop && [[ "$rule" =~ DROP ]]; then continue; fi
|
||||||
ui::firewall_rule "$rule"
|
ui::firewall_rule "$rule"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
@ -37,39 +37,50 @@ function cmd::inspect::_section() {
|
||||||
printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title"
|
printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::inspect::_row() {
|
|
||||||
local label="$1" value="$2"
|
|
||||||
printf " %-20s %s\n" "${label}:" "$value"
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::inspect::_peer_info() {
|
function cmd::inspect::_peer_info() {
|
||||||
local name="$1"
|
local name="${1:-}"
|
||||||
local ip type rule status endpoint last_seen tunnel public_key
|
|
||||||
|
|
||||||
|
local ip type rule public_key allowed_ips
|
||||||
ip=$(peers::get_ip "$name")
|
ip=$(peers::get_ip "$name")
|
||||||
type=$(peers::get_type "$name")
|
type=$(peers::get_type "$name")
|
||||||
rule=$(peers::get_meta "$name" "rule")
|
rule=$(peers::get_meta "$name" "rule")
|
||||||
public_key=$(keys::public "$name" 2>/dev/null)
|
public_key=$(keys::public "$name" 2>/dev/null || echo "")
|
||||||
|
allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" \
|
||||||
|
2>/dev/null | cut -d'=' -f2- | xargs)
|
||||||
|
|
||||||
# Status + endpoint + last seen — reuse list helpers
|
# Status
|
||||||
status=$(cmd::list::format_status "$name" "$public_key" "$ip")
|
local handshake_ts is_blocked last_ts
|
||||||
last_seen=$(cmd::list::format_last_seen "$name" "$public_key" "$ip")
|
handshake_ts=$(monitor::get_handshake_ts "$public_key")
|
||||||
|
peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
|
||||||
|
last_ts=$(monitor::last_attempt "$name")
|
||||||
|
|
||||||
|
local status last_seen endpoint
|
||||||
|
status=$(peers::format_status "$name" "$public_key" \
|
||||||
|
"$is_blocked" "false" "$handshake_ts" "$last_ts")
|
||||||
|
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
||||||
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
endpoint=$(monitor::get_cached_endpoint "$name")
|
endpoint=$(monitor::get_cached_endpoint "$name")
|
||||||
|
|
||||||
# Tunnel mode from AllowedIPs in conf
|
local activity_total
|
||||||
local allowed_ips
|
activity_total=$(peers::format_activity_total "$public_key")
|
||||||
allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" 2>/dev/null | cut -d'=' -f2- | xargs)
|
|
||||||
|
local activity_current
|
||||||
|
activity_current=$(peers::format_activity_current "$public_key")
|
||||||
|
|
||||||
|
local subtype
|
||||||
|
subtype=$(peers::get_meta "$name" "subtype")
|
||||||
|
|
||||||
cmd::inspect::_section "Client"
|
cmd::inspect::_section "Client"
|
||||||
cmd::inspect::_row "Name" "$name"
|
ui::row "Name" "$name"
|
||||||
cmd::inspect::_row "IP" "$ip"
|
ui::row "IP" "$ip"
|
||||||
cmd::inspect::_row "Type" "$(cmd::list::display_type "$name" "$type")"
|
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
|
||||||
cmd::inspect::_row "Rule" "${rule:-—}"
|
ui::row "Rule" "${rule:-—}"
|
||||||
cmd::inspect::_row "Status" "$(echo -e "$status")"
|
ui::row "Status" "$(echo -e "$status")"
|
||||||
cmd::inspect::_row "Endpoint" "${endpoint:-—}"
|
ui::row "Endpoint" "${endpoint:-—}"
|
||||||
cmd::inspect::_row "Last seen" "$last_seen"
|
ui::row "Last seen" "$last_seen"
|
||||||
cmd::inspect::_row "AllowedIPs" "$allowed_ips"
|
ui::row "AllowedIPs" "$allowed_ips"
|
||||||
cmd::inspect::_row "Public key" "${public_key:-—}"
|
ui::row "Public key" "${public_key:-—}"
|
||||||
|
ui::row "Activity" "Total: $activity_total | Current: $activity_current"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::inspect::_rule_info() {
|
function cmd::inspect::_rule_info() {
|
||||||
|
|
|
||||||
|
|
@ -51,125 +51,6 @@ Examples:
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Status / Display helpers
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
function cmd::list::_is_connected() {
|
|
||||||
local ts="$1"
|
|
||||||
[[ -z "$ts" || "$ts" == "0" ]] && return 1
|
|
||||||
local now diff
|
|
||||||
now=$(date +%s)
|
|
||||||
diff=$(( now - ts ))
|
|
||||||
(( diff < 180 ))
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::list::_is_attempting() {
|
|
||||||
local last_ts="$1"
|
|
||||||
[[ -z "$last_ts" ]] && return 1
|
|
||||||
local now attempt_ts diff
|
|
||||||
now=$(date +%s)
|
|
||||||
attempt_ts=$(json::iso_to_ts "$last_ts")
|
|
||||||
[[ -z "$attempt_ts" || "$attempt_ts" == "0" ]] && return 1
|
|
||||||
diff=$(( now - attempt_ts ))
|
|
||||||
(( diff < 180 ))
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::list::_format_last_seen() {
|
|
||||||
local name="$1"
|
|
||||||
local pubkey="$2"
|
|
||||||
local is_blocked="${3:-0}"
|
|
||||||
local last_ts="${4:-0}"
|
|
||||||
local last_evt="${5:-0}"
|
|
||||||
local handshake_ts="${6:-0}"
|
|
||||||
|
|
||||||
if [[ "$is_blocked" == "true" ]]; then
|
|
||||||
if [[ -n "$last_ts" && "$last_ts" != "0" && "$last_ts" != "null" ]]; then
|
|
||||||
local formatted
|
|
||||||
formatted=$(fmt::datetime_iso "$last_ts")
|
|
||||||
echo "${formatted} (dropped)"
|
|
||||||
else
|
|
||||||
echo "—"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [[ -z "$handshake_ts" || "$handshake_ts" == "0" ]]; then
|
|
||||||
echo "—"
|
|
||||||
else
|
|
||||||
local formatted
|
|
||||||
formatted=$(fmt::datetime "$handshake_ts")
|
|
||||||
echo "${formatted} (handshake)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::list::_format_status() {
|
|
||||||
local name="$1"
|
|
||||||
local pubkey="$2"
|
|
||||||
local is_blocked="${3:-false}"
|
|
||||||
local is_restricted="${4:-false}"
|
|
||||||
local handshake_ts="${5:-0}"
|
|
||||||
local last_ts="${6:-}"
|
|
||||||
local connected=false modifier="" color
|
|
||||||
|
|
||||||
if [[ "$is_blocked" == "true" ]]; then
|
|
||||||
cmd::list::_is_attempting "$last_ts" && connected=true
|
|
||||||
modifier=" (blocked)"
|
|
||||||
color="\033[1;31m"
|
|
||||||
elif [[ "$is_restricted" == "true" ]]; then
|
|
||||||
cmd::list::_is_connected "$handshake_ts" && connected=true
|
|
||||||
modifier=" (restricted)"
|
|
||||||
color="\033[1;33m"
|
|
||||||
else
|
|
||||||
cmd::list::_is_connected "$handshake_ts" && connected=true
|
|
||||||
modifier=""
|
|
||||||
if $connected; then
|
|
||||||
color="\033[1;32m"
|
|
||||||
else
|
|
||||||
color="\033[0;37m"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
local conn_str
|
|
||||||
$connected && conn_str="online" || conn_str="offline"
|
|
||||||
echo -e "${color}${conn_str}${modifier}\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
function peers::get_type() {
|
|
||||||
local ip="$1"
|
|
||||||
local type="unknown"
|
|
||||||
for t in $(config::device_types); do
|
|
||||||
local subnet
|
|
||||||
subnet=$(config::subnet_for "$t")
|
|
||||||
if string::starts_with "$ip" "${subnet}."; then
|
|
||||||
type="$t"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "$type"
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::list::display_type() {
|
|
||||||
local name="${1:-0}"
|
|
||||||
local type="${2:-0}"
|
|
||||||
local subtype="${3:-}"
|
|
||||||
if config::is_guest_type "$type" && [[ -n "$subtype" ]]; then
|
|
||||||
echo "guest/${subtype}"
|
|
||||||
elif config::is_guest_type "$type"; then
|
|
||||||
echo "guest"
|
|
||||||
else
|
|
||||||
echo "$type"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::list::pad_status() {
|
|
||||||
local status="$1"
|
|
||||||
local width="${2:-25}"
|
|
||||||
local visible
|
|
||||||
visible=$(echo -e "$status" | sed 's/\x1b\[[0-9;]*m//g')
|
|
||||||
local pad=$(( width - ${#visible} ))
|
|
||||||
printf "%b%${pad}s" "$status" ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Header / Footer
|
# Header / Footer
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -224,7 +105,7 @@ function cmd::list::_render_summary() {
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::show_client() {
|
function cmd::list::show_client() {
|
||||||
local name="$1"
|
local name="${1:-}"
|
||||||
local conf
|
local conf
|
||||||
conf="$(ctx::clients)/${name}.conf"
|
conf="$(ctx::clients)/${name}.conf"
|
||||||
|
|
||||||
|
|
@ -233,16 +114,26 @@ function cmd::list::show_client() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local ip allowed_ips public_key
|
local ip
|
||||||
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
|
|
||||||
|
local allowed_ips
|
||||||
allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs)
|
allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs)
|
||||||
|
|
||||||
|
local public_key
|
||||||
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
local type
|
local type
|
||||||
type=$(peers::get_type "$ip")
|
type=$(peers::get_type_from_ip "$ip")
|
||||||
|
|
||||||
|
local subtype
|
||||||
|
subtype=$(peers::get_meta "$name" "subtype")
|
||||||
|
|
||||||
local endpoint="—"
|
local endpoint="—"
|
||||||
if peers::is_blocked "$name"; then
|
local is_blocked="false"
|
||||||
|
peers::is_blocked "$name" && is_blocked="true"
|
||||||
|
|
||||||
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
local ep
|
local ep
|
||||||
ep=$(monitor::last_endpoint "$name")
|
ep=$(monitor::last_endpoint "$name")
|
||||||
[[ -n "$ep" ]] && endpoint="$ep"
|
[[ -n "$ep" ]] && endpoint="$ep"
|
||||||
|
|
@ -252,22 +143,18 @@ function cmd::list::show_client() {
|
||||||
[[ -n "$ep" ]] && endpoint="$ep"
|
[[ -n "$ep" ]] && endpoint="$ep"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get handshake and last attempt for status/last_seen
|
|
||||||
local handshake_ts
|
local handshake_ts
|
||||||
handshake_ts=$(wg show "$(config::interface)" latest-handshakes 2>/dev/null \
|
handshake_ts=$(monitor::get_handshake_ts "$public_key")
|
||||||
| grep "^${public_key}" | awk '{print $2}')
|
|
||||||
handshake_ts="${handshake_ts:-0}"
|
|
||||||
|
|
||||||
local last_ts
|
local last_ts
|
||||||
last_ts=$(monitor::last_attempt "$name")
|
last_ts=$(monitor::last_attempt "$name")
|
||||||
|
|
||||||
local is_blocked="false"
|
local status
|
||||||
peers::is_blocked "$name" && is_blocked="true"
|
status=$(peers::format_status "$name" "$public_key" \
|
||||||
|
|
||||||
local status last_seen
|
|
||||||
status=$(cmd::list::_format_status "$name" "$public_key" \
|
|
||||||
"$is_blocked" "false" "$handshake_ts" "$last_ts")
|
"$is_blocked" "false" "$handshake_ts" "$last_ts")
|
||||||
last_seen=$(cmd::list::_format_last_seen "$name" "$public_key" \
|
|
||||||
|
local last_seen
|
||||||
|
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
||||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
|
|
||||||
local block_file
|
local block_file
|
||||||
|
|
@ -287,7 +174,7 @@ function cmd::list::show_client() {
|
||||||
|
|
||||||
ui::section "Client: ${name}"
|
ui::section "Client: ${name}"
|
||||||
ui::row "IP" "$ip"
|
ui::row "IP" "$ip"
|
||||||
ui::row "Type" "$type"
|
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
|
||||||
ui::row "Status" "$(echo -e "$status")"
|
ui::row "Status" "$(echo -e "$status")"
|
||||||
ui::row "Endpoint" "$endpoint"
|
ui::row "Endpoint" "$endpoint"
|
||||||
ui::row "Last seen" "$last_seen"
|
ui::row "Last seen" "$last_seen"
|
||||||
|
|
@ -305,14 +192,6 @@ function cmd::list::show_client() {
|
||||||
printf "\n"
|
printf "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Keep old format_status/format_last_seen for backward compat
|
|
||||||
function cmd::list::format_status() { cmd::list::_format_status "$@"; }
|
|
||||||
function cmd::list::format_last_seen() { cmd::list::_format_last_seen "$@"; }
|
|
||||||
function cmd::list::is_blocked() { peers::is_blocked "$1"; }
|
|
||||||
function cmd::list::is_restricted() { [[ -f "$(ctx::block::path "${1}.block")" ]]; }
|
|
||||||
function cmd::list::is_connected() { cmd::list::_is_connected "$@"; }
|
|
||||||
function cmd::list::is_attempting() { cmd::list::_is_attempting "$@"; }
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Run
|
# Run
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -404,7 +283,7 @@ function cmd::list::_iter_confs() {
|
||||||
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
fi
|
fi
|
||||||
local type
|
local type
|
||||||
type=$(peers::get_type "$ip")
|
type=$(peers::get_type_from_ip "$ip")
|
||||||
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||||
"$callback" "$client_name" "$ip" "$type"
|
"$callback" "$client_name" "$ip" "$type"
|
||||||
done
|
done
|
||||||
|
|
@ -420,8 +299,8 @@ function cmd::list::_render_row() {
|
||||||
local last_ts="${p_last_ts[$client_name]:-}"
|
local last_ts="${p_last_ts[$client_name]:-}"
|
||||||
|
|
||||||
# Apply status filters
|
# Apply status filters
|
||||||
if $online_only; then cmd::list::_is_connected "$handshake_ts" || return 0; fi
|
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
if $offline_only; then cmd::list::_is_connected "$handshake_ts" && return 0; fi
|
if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
|
if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
|
||||||
if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
||||||
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||||
|
|
@ -434,12 +313,11 @@ function cmd::list::_render_row() {
|
||||||
|
|
||||||
# Format display values
|
# Format display values
|
||||||
local status last_seen display_type rule group_display
|
local status last_seen display_type rule group_display
|
||||||
status=$(cmd::list::_format_status "$client_name" "$pubkey" \
|
status=$(peers::format_status "$client_name" "$pubkey" \
|
||||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
last_seen=$(cmd::list::_format_last_seen "$client_name" "$pubkey" \
|
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
||||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
display_type=$(cmd::list::display_type "$client_name" "$type" \
|
display_type=$(peers::display_type "$type" "${p_subtypes[$client_name]:-}")
|
||||||
"${p_subtypes[$client_name]:-}")
|
|
||||||
rule="${p_rules[$client_name]:-—}"
|
rule="${p_rules[$client_name]:-—}"
|
||||||
|
|
||||||
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
||||||
|
|
@ -456,7 +334,7 @@ function cmd::list::_render_row() {
|
||||||
|
|
||||||
# Pad status
|
# Pad status
|
||||||
local padded_status
|
local padded_status
|
||||||
padded_status=$(cmd::list::pad_status "$status" 25)
|
padded_status=$(ui::pad_status "$status" 25)
|
||||||
|
|
||||||
# Render row
|
# Render row
|
||||||
if $has_groups; then
|
if $has_groups; then
|
||||||
|
|
@ -545,6 +423,15 @@ function cmd::list::_precompute_all() {
|
||||||
[[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
|
[[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
|
||||||
done < <(json::peer_group_map "$groups_dir")
|
done < <(json::peer_group_map "$groups_dir")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Transfer/activity data — keyed by pubkey
|
||||||
|
declare -gA p_rx=() p_tx=() p_activity=()
|
||||||
|
while IFS="|" read -r pubkey rx tx level; do
|
||||||
|
[[ -z "$pubkey" ]] && continue
|
||||||
|
p_rx["$pubkey"]="$rx"
|
||||||
|
p_tx["$pubkey"]="$tx"
|
||||||
|
p_activity["$pubkey"]="$level"
|
||||||
|
done < <(json::peer_transfer "$(config::interface)")
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::list::_build_filter_desc() {
|
function cmd::list::_build_filter_desc() {
|
||||||
|
|
|
||||||
|
|
@ -11,35 +11,51 @@ 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 --all
|
||||||
|
flag::register --before
|
||||||
|
flag::register --force
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::help() {
|
function cmd::logs::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl logs [options]
|
Usage: wgctl logs [subcommand] [options]
|
||||||
|
|
||||||
Show WireGuard and firewall activity logs.
|
Show or manage WireGuard and firewall activity logs.
|
||||||
|
|
||||||
Options:
|
Subcommands:
|
||||||
|
show (default) Show activity logs
|
||||||
|
remove, rm Remove log entries
|
||||||
|
|
||||||
|
Options for show:
|
||||||
--name <name> Filter by client name
|
--name <name> Filter by client name
|
||||||
--type <type> Filter by device type
|
--type <type> Filter by device type
|
||||||
--since <time> Time filter (e.g. 1h, 24h, 7d)
|
--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
|
||||||
|
|
||||||
|
Options for remove:
|
||||||
|
--name <name> Remove entries for specific peer
|
||||||
|
--all Remove all log entries
|
||||||
|
--fw Remove only firewall events
|
||||||
|
--wg Remove only WireGuard events
|
||||||
|
--before <days> Remove entries older than N days
|
||||||
|
--force Skip confirmation
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl logs
|
wgctl logs
|
||||||
wgctl logs --name guest-test
|
wgctl logs --name phone-nuno
|
||||||
wgctl logs --type guest
|
|
||||||
wgctl logs --since 1h
|
|
||||||
wgctl logs --fw --limit 100
|
wgctl logs --fw --limit 100
|
||||||
|
wgctl logs --follow
|
||||||
|
wgctl logs remove --name phone-nuno
|
||||||
|
wgctl logs remove --all --force
|
||||||
|
wgctl logs remove --before 7
|
||||||
|
wgctl logs remove --fw --before 1
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::run() {
|
function cmd::logs::run() {
|
||||||
local subcmd="${1:-show}"
|
local subcmd="${1:-show}"
|
||||||
|
|
||||||
# Check if first arg is a flag
|
|
||||||
if [[ "$subcmd" == --* ]]; then
|
if [[ "$subcmd" == --* ]]; then
|
||||||
subcmd="show"
|
subcmd="show"
|
||||||
else
|
else
|
||||||
|
|
@ -59,14 +75,13 @@ function cmd::logs::run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::show() {
|
function cmd::logs::show() {
|
||||||
local name="" type="" since="" 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
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
--type) type="$2"; shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
--since) since="$2"; shift 2 ;;
|
|
||||||
--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 ;;
|
||||||
|
|
@ -104,10 +119,8 @@ 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 filter_peers="${6:-}"
|
||||||
|
|
||||||
local clients_dir
|
local clients_dir
|
||||||
clients_dir="$(ctx::clients)"
|
clients_dir="$(ctx::clients)"
|
||||||
|
|
||||||
local wg_log="$WG_EVENTS_LOG"
|
local wg_log="$WG_EVENTS_LOG"
|
||||||
local fw_log="$FW_EVENTS_LOG"
|
local fw_log="$FW_EVENTS_LOG"
|
||||||
$fw_only && wg_log=""
|
$fw_only && wg_log=""
|
||||||
|
|
@ -139,16 +152,22 @@ function cmd::logs::follow() {
|
||||||
printf " %-20s %-8s %-20s %-25s %b\n" \
|
printf " %-20s %-8s %-20s %-25s %b\n" \
|
||||||
"$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event"
|
"$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event"
|
||||||
fi
|
fi
|
||||||
done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" "$clients_dir" "$filter_peers")
|
done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" \
|
||||||
|
"$clients_dir" "$filter_peers")
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::remove() {
|
function cmd::logs::remove() {
|
||||||
local name="" type="" force=false
|
local name="" type="" before="" force=false
|
||||||
|
local fw_only=false wg_only=false all=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
--type) type="$2"; shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
|
--before) before="$2"; shift 2 ;;
|
||||||
|
--fw) fw_only=true; shift ;;
|
||||||
|
--wg) wg_only=true; shift ;;
|
||||||
|
--all) all=true; shift ;;
|
||||||
--force) force=true; shift ;;
|
--force) force=true; shift ;;
|
||||||
--help) cmd::logs::help; return ;;
|
--help) cmd::logs::help; return ;;
|
||||||
*)
|
*)
|
||||||
|
|
@ -158,50 +177,56 @@ function cmd::logs::remove() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$name" ]]; then
|
# Validate — need at least one filter
|
||||||
log::error "Missing required flag: --name"
|
if ! $all && [[ -z "$name" && -z "$before" ]]; then
|
||||||
|
log::error "Specify --name, --before, or --all"
|
||||||
|
cmd::logs::help
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local filter_ip=""
|
||||||
|
if [[ -n "$name" ]]; then
|
||||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
filter_ip=$(peers::get_ip "$name")
|
||||||
local client_ip
|
|
||||||
client_ip=$(peers::get_ip "$name")
|
|
||||||
|
|
||||||
local before_wg before_fw after_wg after_fw
|
|
||||||
before_wg=$(wc -l < "$WG_EVENTS_LOG" 2>/dev/null || echo 0)
|
|
||||||
before_fw=$(wc -l < "$FW_EVENTS_LOG" 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
json::remove_events "$WG_EVENTS_LOG" "$name"
|
|
||||||
json::remove_events "$FW_EVENTS_LOG" "$client_ip"
|
|
||||||
|
|
||||||
after_wg=$(wc -l < "$WG_EVENTS_LOG" 2>/dev/null || echo 0)
|
|
||||||
after_fw=$(wc -l < "$FW_EVENTS_LOG" 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
local removed=$(( (before_wg - after_wg) + (before_fw - after_fw) ))
|
|
||||||
if [[ "$removed" -eq 0 ]]; then
|
|
||||||
log::wg_warning "No log entries found for: ${name}"
|
|
||||||
return 0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Build description for confirmation
|
||||||
|
local desc=""
|
||||||
|
$all && desc="all entries"
|
||||||
|
[[ -n "$name" ]] && desc="entries for '${name}'"
|
||||||
|
[[ -n "$before" ]] && desc="${desc:+$desc, }entries older than ${before} days"
|
||||||
|
$fw_only && desc="${desc} (fw only)"
|
||||||
|
$wg_only && desc="${desc} (wg only)"
|
||||||
|
|
||||||
if ! $force; then
|
if ! $force; then
|
||||||
read -r -p "Remove all log entries for '${name}'? [y/N] " confirm
|
read -r -p "Remove ${desc}? [y/N] " confirm
|
||||||
case "$confirm" in
|
case "$confirm" in
|
||||||
[yY][eE][sS]|[yY]) ;;
|
[yY][eE][sS]|[yY]) ;;
|
||||||
*) log::info "Aborted"; return 0 ;;
|
*) log::info "Aborted"; return 0 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log::wg_success "Removed ${removed} log entries for: ${name}"
|
local result
|
||||||
|
result=$(json::remove_events_filtered \
|
||||||
|
"$WG_EVENTS_LOG" "$FW_EVENTS_LOG" \
|
||||||
|
"${name:-}" "${filter_ip:-}" \
|
||||||
|
"$fw_only" "$wg_only" \
|
||||||
|
"${before:-}")
|
||||||
|
|
||||||
json::remove_events "$WG_EVENTS_LOG" "$name"
|
local removed_wg removed_fw
|
||||||
json::remove_events "$FW_EVENTS_LOG" "$client_ip"
|
IFS="|" read -r removed_wg removed_fw <<< "$result"
|
||||||
|
local total=$(( removed_wg + removed_fw ))
|
||||||
|
|
||||||
log::wg_success "Removed log entries for: ${name}"
|
if [[ "$total" -eq 0 ]]; then
|
||||||
|
log::wg_warning "No log entries found matching the criteria"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log::wg_success "Removed ${total} log entries (wg: ${removed_wg}, fw: ${removed_fw})"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::show_wg_events() {
|
function cmd::logs::show_wg_events() {
|
||||||
local filter_ip="$1" filter_name="$2" filter_type="$3" limit="$4"
|
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" limit="${4:-50}"
|
||||||
|
|
||||||
[[ ! -f "$WG_EVENTS_LOG" ]] && return 0
|
[[ ! -f "$WG_EVENTS_LOG" ]] && return 0
|
||||||
|
|
||||||
|
|
@ -214,8 +239,8 @@ function cmd::logs::show_wg_events() {
|
||||||
[[ -z "$ts" ]] && continue
|
[[ -z "$ts" ]] && continue
|
||||||
local colored_event
|
local colored_event
|
||||||
case "$event" in
|
case "$event" in
|
||||||
attempt) colored_event="\033[1;31mattempt\033[0m" ;;
|
attempt*) colored_event="\033[1;31m${event}\033[0m" ;;
|
||||||
handshake) colored_event="\033[1;32mhandshake\033[0m" ;;
|
handshake*) colored_event="\033[1;32m${event}\033[0m" ;;
|
||||||
*) colored_event="$event" ;;
|
*) colored_event="$event" ;;
|
||||||
esac
|
esac
|
||||||
printf " %-20s %-20s %-18s %b\n" "$ts" "$client" "$endpoint" "$colored_event"
|
printf " %-20s %-20s %-18s %b\n" "$ts" "$client" "$endpoint" "$colored_event"
|
||||||
|
|
@ -227,7 +252,7 @@ function cmd::logs::show_wg_events() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::show_fw_events() {
|
function cmd::logs::show_fw_events() {
|
||||||
local filter_ip="$1" filter_name="$2" filter_type="$3" limit="$4"
|
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" limit="${4:-50}"
|
||||||
|
|
||||||
[[ ! -f "$FW_EVENTS_LOG" ]] && return 0
|
[[ ! -f "$FW_EVENTS_LOG" ]] && return 0
|
||||||
|
|
||||||
|
|
@ -240,14 +265,15 @@ function cmd::logs::show_fw_events() {
|
||||||
[[ -z "$ts" ]] && continue
|
[[ -z "$ts" ]] && continue
|
||||||
local colored_proto
|
local colored_proto
|
||||||
case "$proto" in
|
case "$proto" in
|
||||||
tcp) colored_proto="\033[1;33mtcp\033[0m" ;;
|
tcp*) colored_proto="\033[1;33m${proto}\033[0m" ;;
|
||||||
udp) colored_proto="\033[1;36mudp\033[0m" ;;
|
udp*) colored_proto="\033[1;36m${proto}\033[0m" ;;
|
||||||
icmp) colored_proto="\033[0;37micmp\033[0m" ;;
|
icmp*) colored_proto="\033[0;37m${proto}\033[0m" ;;
|
||||||
*) colored_proto="$proto" ;;
|
*) colored_proto="$proto" ;;
|
||||||
esac
|
esac
|
||||||
printf " %-20s %-18s %-25s %b\n" "$ts" "$client" "$dst" "$colored_proto"
|
printf " %-20s %-18s %-25s %b\n" "$ts" "$client" "$dst" "$colored_proto"
|
||||||
found=true
|
found=true
|
||||||
done < <(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" "$(ctx::clients)" "$limit")
|
done < <(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
||||||
|
"$(ctx::clients)" "$limit")
|
||||||
|
|
||||||
$found || printf " —\n"
|
$found || printf " —\n"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
|
|
||||||
254
commands/logs.command.sh.bak
Normal file
254
commands/logs.command.sh.bak
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
FW_EVENTS_LOG="$(ctx::fw_events_log)"
|
||||||
|
WG_EVENTS_LOG="$(ctx::events_log)"
|
||||||
|
|
||||||
|
function cmd::logs::on_load() {
|
||||||
|
flag::register --name
|
||||||
|
flag::register --type
|
||||||
|
flag::register --since
|
||||||
|
flag::register --limit
|
||||||
|
flag::register --fw
|
||||||
|
flag::register --wg
|
||||||
|
flag::register --follow
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::logs::help() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: wgctl logs [options]
|
||||||
|
|
||||||
|
Show WireGuard and firewall activity logs.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--name <name> Filter by client name
|
||||||
|
--type <type> Filter by device type
|
||||||
|
--since <time> Time filter (e.g. 1h, 24h, 7d)
|
||||||
|
--limit <n> Max results per source (default 50)
|
||||||
|
--fw Show only firewall drops
|
||||||
|
--wg Show only WireGuard events
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
wgctl logs
|
||||||
|
wgctl logs --name guest-test
|
||||||
|
wgctl logs --type guest
|
||||||
|
wgctl logs --since 1h
|
||||||
|
wgctl logs --fw --limit 100
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::logs::run() {
|
||||||
|
local subcmd="${1:-show}"
|
||||||
|
|
||||||
|
# Check if first arg is a flag
|
||||||
|
if [[ "$subcmd" == --* ]]; then
|
||||||
|
subcmd="show"
|
||||||
|
else
|
||||||
|
shift || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$subcmd" in
|
||||||
|
show) cmd::logs::show "$@" ;;
|
||||||
|
remove|rm|del) cmd::logs::remove "$@" ;;
|
||||||
|
help) cmd::logs::help ;;
|
||||||
|
*)
|
||||||
|
log::error "Unknown subcommand: '${subcmd}'"
|
||||||
|
cmd::logs::help
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::logs::show() {
|
||||||
|
local name="" type="" since="" limit=50
|
||||||
|
local fw_only=false wg_only=false follow=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--name) name="$2"; shift 2 ;;
|
||||||
|
--type) type="$2"; shift 2 ;;
|
||||||
|
--since) since="$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 ;;
|
||||||
|
*)
|
||||||
|
log::error "Unknown flag: $1"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$name" && -n "$type" ]]; then
|
||||||
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local filter_ip=""
|
||||||
|
if [[ -n "$name" ]]; then
|
||||||
|
filter_ip=$(peers::get_ip "$name")
|
||||||
|
[[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $follow; then
|
||||||
|
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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})"
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::logs::remove() {
|
||||||
|
local name="" type="" force=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--name) name="$2"; shift 2 ;;
|
||||||
|
--type) type="$2"; shift 2 ;;
|
||||||
|
--force) force=true; shift ;;
|
||||||
|
--help) cmd::logs::help; return ;;
|
||||||
|
*)
|
||||||
|
log::error "Unknown flag: $1"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$name" ]]; then
|
||||||
|
log::error "Missing required flag: --name"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
|
local client_ip
|
||||||
|
client_ip=$(peers::get_ip "$name")
|
||||||
|
|
||||||
|
local before_wg before_fw after_wg after_fw
|
||||||
|
before_wg=$(wc -l < "$WG_EVENTS_LOG" 2>/dev/null || echo 0)
|
||||||
|
before_fw=$(wc -l < "$FW_EVENTS_LOG" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
json::remove_events "$WG_EVENTS_LOG" "$name"
|
||||||
|
json::remove_events "$FW_EVENTS_LOG" "$client_ip"
|
||||||
|
|
||||||
|
after_wg=$(wc -l < "$WG_EVENTS_LOG" 2>/dev/null || echo 0)
|
||||||
|
after_fw=$(wc -l < "$FW_EVENTS_LOG" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
local removed=$(( (before_wg - after_wg) + (before_fw - after_fw) ))
|
||||||
|
if [[ "$removed" -eq 0 ]]; then
|
||||||
|
log::wg_warning "No log entries found for: ${name}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! $force; then
|
||||||
|
read -r -p "Remove all log entries for '${name}'? [y/N] " confirm
|
||||||
|
case "$confirm" in
|
||||||
|
[yY][eE][sS]|[yY]) ;;
|
||||||
|
*) log::info "Aborted"; return 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
log::wg_success "Removed ${removed} log entries for: ${name}"
|
||||||
|
|
||||||
|
json::remove_events "$WG_EVENTS_LOG" "$name"
|
||||||
|
json::remove_events "$FW_EVENTS_LOG" "$client_ip"
|
||||||
|
|
||||||
|
log::wg_success "Removed log entries for: ${name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::logs::show_wg_events() {
|
||||||
|
local filter_ip="$1" filter_name="$2" filter_type="$3" limit="$4"
|
||||||
|
|
||||||
|
[[ ! -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;31mattempt\033[0m" ;;
|
||||||
|
handshake) colored_event="\033[1;32mhandshake\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"
|
||||||
|
|
||||||
|
[[ ! -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;33mtcp\033[0m" ;;
|
||||||
|
udp) colored_proto="\033[1;36mudp\033[0m" ;;
|
||||||
|
icmp) colored_proto="\033[0;37micmp\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"
|
||||||
|
}
|
||||||
|
|
@ -89,6 +89,7 @@ function cmd::rule::run() {
|
||||||
unassign) cmd::rule::unassign "$@" ;;
|
unassign) cmd::rule::unassign "$@" ;;
|
||||||
migrate) cmd::rule::migrate "$@" ;;
|
migrate) cmd::rule::migrate "$@" ;;
|
||||||
reapply) cmd::rule::reapply "$@" ;;
|
reapply) cmd::rule::reapply "$@" ;;
|
||||||
|
inspect) cmd::rule::inspect "$@" ;;
|
||||||
help) cmd::rule::help ;;
|
help) cmd::rule::help ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown subcommand: '${subcmd}'"
|
log::error "Unknown subcommand: '${subcmd}'"
|
||||||
|
|
@ -102,6 +103,16 @@ function cmd::rule::run() {
|
||||||
# List
|
# 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::list() {
|
function cmd::rule::list() {
|
||||||
local rules_dir
|
local rules_dir
|
||||||
rules_dir="$(ctx::rules)"
|
rules_dir="$(ctx::rules)"
|
||||||
|
|
@ -113,16 +124,63 @@ function cmd::rule::list() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log::section "Firewall Rules"
|
log::section "Firewall Rules"
|
||||||
printf "\n %-20s %-45s %-8s %-8s %s\n" \
|
printf "\n %-20s %-40s %-8s %-8s %s\n" \
|
||||||
"NAME" "DESCRIPTION" "ALLOWS" "BLOCKS" "PEERS"
|
"NAME" "DESCRIPTION" \
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..95})"
|
"$(ui::center "ALLOWS" 8)" \
|
||||||
|
"$(ui::center "BLOCKS" 8)" \
|
||||||
|
"PEERS"
|
||||||
|
local divider
|
||||||
|
divider=$(printf '─%.0s' {1..88})
|
||||||
|
printf " %s\n" "$divider"
|
||||||
|
|
||||||
while IFS="|" read -r name desc n_allows n_blocks peer_count; do
|
local printing_base=false
|
||||||
|
local current_group=""
|
||||||
|
|
||||||
|
while IFS="|" read -r name desc n_allows n_blocks peer_count extends is_base group; do
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
local short_desc="${desc:0:43}"
|
|
||||||
[[ ${#desc} -gt 43 ]] && short_desc="${short_desc}..."
|
# Base rules section header
|
||||||
printf " %-20s %-45s %-8s %-8s %s\n" \
|
if [[ "$is_base" == "True" && "$printing_base" == "false" ]]; then
|
||||||
"$name" "${short_desc:-—}" "$n_allows" "$n_blocks" "${peer_count} peers"
|
local bdashes
|
||||||
|
bdashes=$(printf '─%.0s' {1..74})
|
||||||
|
printf "\n \033[0;37m── Base Rules \033[0m%s\n" "$bdashes"
|
||||||
|
printing_base=true
|
||||||
|
current_group="" # reset group tracking for base section
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Group header — only for non-base rules
|
||||||
|
if [[ "$is_base" == "False" && "$group" != "$current_group" ]]; then
|
||||||
|
if [[ -n "$group" ]]; then
|
||||||
|
printf "\n \033[0;36m▸ %s\033[0m\n" "$group"
|
||||||
|
elif [[ -n "$current_group" ]]; then
|
||||||
|
# Switching from grouped back to ungrouped
|
||||||
|
printf "\n"
|
||||||
|
fi
|
||||||
|
current_group="$group"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local short_desc="${desc:0:35}"
|
||||||
|
[[ ${#desc} -gt 35 ]] && short_desc="${short_desc}..."
|
||||||
|
|
||||||
|
local desc_col_width=40
|
||||||
|
[[ "${short_desc:-—}" == "—" ]] && desc_col_width=42
|
||||||
|
|
||||||
|
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 IMMEDIATELY after the rule row
|
||||||
|
if [[ -n "$extends" ]]; then
|
||||||
|
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
|
||||||
|
printf "\n" # blank line after rule with extends
|
||||||
|
fi
|
||||||
|
|
||||||
done < <(json::rule_list_data "$rules_dir" "$(ctx::meta)")
|
done < <(json::rule_list_data "$rules_dir" "$(ctx::meta)")
|
||||||
|
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
|
@ -299,6 +357,60 @@ function cmd::rule::show() {
|
||||||
# printf "\n"
|
# printf "\n"
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Inspect
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::rule::inspect() {
|
||||||
|
local name=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
|
||||||
|
--help) cmd::rule::help; return ;;
|
||||||
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||||
|
rule::require_exists "$name" || return 1
|
||||||
|
|
||||||
|
log::section "Rule Inspect: ${name}"
|
||||||
|
|
||||||
|
local prev_section=""
|
||||||
|
while IFS="|" read -r section key value; do
|
||||||
|
[[ -z "$section" ]] && continue
|
||||||
|
|
||||||
|
# Print section header when section changes
|
||||||
|
if [[ "$section" != "$prev_section" ]]; then
|
||||||
|
case "$section" in
|
||||||
|
own)
|
||||||
|
cmd::rule::_show_section "Own Rules" ;;
|
||||||
|
dns)
|
||||||
|
cmd::rule::_show_section "DNS" ;;
|
||||||
|
resolved)
|
||||||
|
cmd::rule::_show_section "Resolved (applied to peers)" ;;
|
||||||
|
inherited:*)
|
||||||
|
local base_name="${section#inherited:}"
|
||||||
|
cmd::rule::_show_section "Inherited: ${base_name}" ;;
|
||||||
|
esac
|
||||||
|
prev_section="$section"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$key" in
|
||||||
|
allow_ip|allow_port)
|
||||||
|
printf " \033[0;32m+\033[0m %s\n" "$value" ;;
|
||||||
|
block_ip|block_port)
|
||||||
|
printf " \033[0;31m-\033[0m %s\n" "$value" ;;
|
||||||
|
dns_redirect)
|
||||||
|
printf " Redirect all DNS → %s\n" "$(config::dns)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
done < <(json::rule_inspect "$(ctx::rules)" "$name")
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Add
|
# Add
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -487,6 +599,7 @@ function cmd::rule::assign() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rule::require_exists "$name" || return 1
|
rule::require_exists "$name" || return 1
|
||||||
|
rule::require_assignable "$name" || return 1
|
||||||
|
|
||||||
# Support --type for peer resolution
|
# Support --type for peer resolution
|
||||||
peer=$(peers::resolve_and_require "$peer" "$type") || return 1
|
peer=$(peers::resolve_and_require "$peer" "$type") || return 1
|
||||||
|
|
|
||||||
|
|
@ -216,10 +216,6 @@ function cmd::test::section_destructive() {
|
||||||
# Add test peer
|
# Add test peer
|
||||||
cmd::test::run_cmd "add phone peer" "added successfully" \
|
cmd::test::run_cmd "add phone peer" "added successfully" \
|
||||||
add --name testunit --type phone
|
add --name testunit --type phone
|
||||||
|
|
||||||
# Debug: show what was created
|
|
||||||
"$WGCTL_BINARY" list | grep testunit >&2
|
|
||||||
|
|
||||||
# Block/unblock
|
# Block/unblock
|
||||||
cmd::test::run_cmd "block peer" "blocked" \
|
cmd::test::run_cmd "block peer" "blocked" \
|
||||||
block --name phone-testunit
|
block --name phone-testunit
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ _CTX_DATA="${_CTX_WG}/.wgctl"
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
_CTX_RULES="${_CTX_DATA}/rules"
|
_CTX_RULES="${_CTX_DATA}/rules"
|
||||||
|
_CTX_RULES_BASE="${_CTX_RULES}/base"
|
||||||
_CTX_GROUPS="${_CTX_DATA}/groups"
|
_CTX_GROUPS="${_CTX_DATA}/groups"
|
||||||
_CTX_BLOCKS="${_CTX_DATA}/blocks"
|
_CTX_BLOCKS="${_CTX_DATA}/blocks"
|
||||||
_CTX_META="${_CTX_DATA}/meta"
|
_CTX_META="${_CTX_DATA}/meta"
|
||||||
|
|
@ -31,6 +32,7 @@ function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
||||||
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
||||||
function ctx::groups() { echo "$_CTX_GROUPS"; }
|
function ctx::groups() { echo "$_CTX_GROUPS"; }
|
||||||
function ctx::rules() { echo "$_CTX_RULES"; }
|
function ctx::rules() { echo "$_CTX_RULES"; }
|
||||||
|
function ctx::rules::base() { echo "$_CTX_RULES_BASE"; }
|
||||||
function ctx::clients() { echo "$_CTX_CLIENTS"; }
|
function ctx::clients() { echo "$_CTX_CLIENTS"; }
|
||||||
function ctx::wg() { echo "$_CTX_WG"; }
|
function ctx::wg() { echo "$_CTX_WG"; }
|
||||||
function ctx::data() { echo "$_CTX_DATA"; }
|
function ctx::data() { echo "$_CTX_DATA"; }
|
||||||
|
|
@ -39,7 +41,6 @@ function ctx::groups() { echo "$_CTX_GROUPS"; }
|
||||||
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
||||||
function ctx::meta() { echo "$_CTX_META"; }
|
function ctx::meta() { echo "$_CTX_META"; }
|
||||||
function ctx::daemon() { echo "$_CTX_DAEMON"; }
|
function ctx::daemon() { echo "$_CTX_DAEMON"; }
|
||||||
|
|
||||||
function ctx::events_log() { echo "$(ctx::daemon)/events.log"; }
|
function ctx::events_log() { echo "$(ctx::daemon)/events.log"; }
|
||||||
function ctx::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; }
|
function ctx::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; }
|
||||||
|
|
||||||
|
|
|
||||||
19
core/json.sh
19
core/json.sh
|
|
@ -33,3 +33,22 @@ function json::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "
|
||||||
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; }
|
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; }
|
||||||
function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; }
|
function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; }
|
||||||
function json::parse_fw_event() { python3 "$JSON_HELPER" parse_fw_event "$@" </dev/null; }
|
function json::parse_fw_event() { python3 "$JSON_HELPER" parse_fw_event "$@" </dev/null; }
|
||||||
|
function json::remove_events_filtered() { python3 "$JSON_HELPER" remove_events_filtered "$@" </dev/null; }
|
||||||
|
function json::rule_resolve() { python3 "$JSON_HELPER" rule_resolve "$@" </dev/null; }
|
||||||
|
function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_field "$@" </dev/null; }
|
||||||
|
function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; }
|
||||||
|
function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; }
|
||||||
|
|
||||||
|
function json::peer_transfer() {
|
||||||
|
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||||
|
ACTIVITY_TOTAL_MED="$(config::activity_total_med)" \
|
||||||
|
ACTIVITY_TOTAL_HIGH="$(config::activity_total_high)" \
|
||||||
|
python3 "$JSON_HELPER" peer_transfer "$@" </dev/null
|
||||||
|
}
|
||||||
|
function json::peer_transfer_delta() {
|
||||||
|
ACTIVITY_CURRENT_LOW="$(config::activity_current_low)" \
|
||||||
|
ACTIVITY_CURRENT_MED="$(config::activity_current_med)" \
|
||||||
|
ACTIVITY_CURRENT_HIGH="$(config::activity_current_high)" \
|
||||||
|
python3 "$JSON_HELPER" peer_transfer_delta "$@" </dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -155,12 +155,10 @@ def events_for(file, ip, limit):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
||||||
"""Format firewall drop events"""
|
"""Format firewall drop events with dedup and counts"""
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
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', '')
|
||||||
|
|
@ -174,7 +172,7 @@ def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
last_seen = {} # (src, dst, port, proto) -> last timestamp
|
last_seen = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(file) as f:
|
with open(file) as f:
|
||||||
|
|
@ -186,36 +184,93 @@ 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
|
||||||
|
|
||||||
# Dedup key
|
|
||||||
dst = e.get('dest_ip', '')
|
dst = e.get('dest_ip', '')
|
||||||
port = e.get('dest_port', '')
|
port = e.get('dest_port', '')
|
||||||
proto = e.get('ip.protocol', 0)
|
proto = e.get('ip.protocol', 0)
|
||||||
key = (src, dst, port, proto)
|
key = (src, dst, port, proto)
|
||||||
|
|
||||||
ts_str = e.get('timestamp', '')
|
ts_str = e.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
ts = datetime.fromisoformat(ts_str).timestamp()
|
ts = datetime.fromisoformat(ts_str).timestamp()
|
||||||
except:
|
except:
|
||||||
ts = 0
|
ts = 0
|
||||||
|
windows = {1: 5, 6: 30, 17: 10}
|
||||||
# Protocol-aware dedup window
|
window = windows.get(proto, 10)
|
||||||
dedup_windows = {1: 5, 6: 30, 17: 10} # icmp=5s, tcp=30s, udp=10s
|
|
||||||
window = dedup_windows.get(proto, 10)
|
|
||||||
|
|
||||||
# Skip if same event within protocol window
|
|
||||||
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:
|
||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for e in events[-int(limit):]:
|
# Dedup consecutive same src+dst+port within 60s 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:
|
||||||
|
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:
|
||||||
|
prev = deduped[-1]
|
||||||
|
try:
|
||||||
|
prev_ts = datetime.fromisoformat(prev.get('timestamp','')).timestamp()
|
||||||
|
except:
|
||||||
|
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:
|
||||||
|
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', '')
|
ts = e.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -230,14 +285,13 @@ def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
||||||
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
|
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 + '-'):
|
if filter_type and not client.startswith(filter_type + '-'):
|
||||||
continue
|
continue
|
||||||
|
count_str = f" (x{count})" if count > 1 else ""
|
||||||
print(f"{ts}|{client}|{dst_str}|{proto}")
|
print(f"{ts}|{client}|{dst_str}|{proto}{count_str}")
|
||||||
|
|
||||||
def wg_events(file, filter_client, filter_type, limit):
|
def wg_events(file, filter_client, filter_type, limit):
|
||||||
"""Format WireGuard events from events.log"""
|
"""Format WireGuard events from events.log with dedup"""
|
||||||
events = []
|
events = []
|
||||||
try:
|
try:
|
||||||
with open(file) as f:
|
with open(file) as f:
|
||||||
|
|
@ -257,7 +311,37 @@ def wg_events(file, filter_client, filter_type, limit):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
for e in events[-int(limit):]:
|
# Dedup consecutive same client+event+endpoint within 60s
|
||||||
|
deduped = []
|
||||||
|
counts = []
|
||||||
|
for e in events:
|
||||||
|
ts_str = e.get('timestamp', '')
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
ts = datetime.fromisoformat(ts_str).timestamp()
|
||||||
|
except:
|
||||||
|
ts = 0
|
||||||
|
client = e.get('client', '')
|
||||||
|
event = e.get('event', '')
|
||||||
|
endpoint = e.get('endpoint', '')
|
||||||
|
key = (client, event, endpoint[:15])
|
||||||
|
|
||||||
|
if deduped and counts:
|
||||||
|
prev = deduped[-1]
|
||||||
|
prev_ts_str = prev.get('timestamp', '')
|
||||||
|
try:
|
||||||
|
prev_ts = datetime.fromisoformat(prev_ts_str).timestamp()
|
||||||
|
except:
|
||||||
|
prev_ts = 0
|
||||||
|
prev_key = (prev.get('client',''), prev.get('event',''), prev.get('endpoint','')[:15])
|
||||||
|
if key == prev_key and (ts - prev_ts) < 60:
|
||||||
|
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', '')
|
ts = e.get('timestamp', '')
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -268,7 +352,8 @@ def wg_events(file, filter_client, filter_type, limit):
|
||||||
client = e.get('client', '—')
|
client = e.get('client', '—')
|
||||||
endpoint = e.get('endpoint', '—')
|
endpoint = e.get('endpoint', '—')
|
||||||
event = e.get('event', '—')
|
event = e.get('event', '—')
|
||||||
print(f"{ts}|{client}|{endpoint}|{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"""
|
||||||
|
|
@ -355,8 +440,6 @@ def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_pe
|
||||||
|
|
||||||
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
||||||
peer_filter = set(filter_peers.split(',')) if filter_peers else set()
|
peer_filter = set(filter_peers.split(',')) if filter_peers else set()
|
||||||
import sys
|
|
||||||
print(f"DEBUG follow_logs: peer_filter={peer_filter}", file=sys.stderr, flush=True)
|
|
||||||
|
|
||||||
# Build ip->name map
|
# Build ip->name map
|
||||||
ip_to_name = {}
|
ip_to_name = {}
|
||||||
|
|
@ -576,10 +659,9 @@ def iso_to_ts(iso_str):
|
||||||
print(0)
|
print(0)
|
||||||
|
|
||||||
def rule_list_data(rules_dir, meta_dir):
|
def rule_list_data(rules_dir, meta_dir):
|
||||||
"""Return all rule data for list display in one call"""
|
"""Return all rule data including base rules and extends"""
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
# Count peers per rule from meta files
|
|
||||||
rule_peer_counts = {}
|
rule_peer_counts = {}
|
||||||
for f in glob.glob(f"{meta_dir}/*.meta"):
|
for f in glob.glob(f"{meta_dir}/*.meta"):
|
||||||
try:
|
try:
|
||||||
|
|
@ -591,20 +673,49 @@ def rule_list_data(rules_dir, meta_dir):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Read each rule file
|
rule_files = (
|
||||||
for rule_file in sorted(glob.glob(f"{rules_dir}/*.rule")):
|
sorted(glob.glob(f"{rules_dir}/*.rule")) +
|
||||||
|
sorted(glob.glob(f"{rules_dir}/base/*.rule"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Collect all data first
|
||||||
|
rules_data = []
|
||||||
|
for rule_file in rule_files:
|
||||||
|
is_base = '/base/' in rule_file
|
||||||
try:
|
try:
|
||||||
with open(rule_file) as f:
|
with open(rule_file) as f:
|
||||||
r = json.load(f)
|
r = json.load(f)
|
||||||
name = r.get('name', '')
|
name = r.get('name', '')
|
||||||
desc = r.get('desc', '')
|
desc = r.get('desc', '')
|
||||||
n_allows = len(r.get('allow_ips', [])) + len(r.get('allow_ports', []))
|
group = r.get('group', '')
|
||||||
n_blocks = len(r.get('block_ips', [])) + len(r.get('block_ports', []))
|
extends = ','.join(r.get('extends', []))
|
||||||
|
resolved = _rule_resolve_internal(rules_dir, name)
|
||||||
|
n_allows = len(resolved.get('allow_ips', [])) + \
|
||||||
|
len(resolved.get('allow_ports', []))
|
||||||
|
n_blocks = len(resolved.get('block_ips', [])) + \
|
||||||
|
len(resolved.get('block_ports', []))
|
||||||
peer_count = rule_peer_counts.get(name, 0)
|
peer_count = rule_peer_counts.get(name, 0)
|
||||||
print(f"{name}|{desc}|{n_allows}|{n_blocks}|{peer_count}")
|
rules_data.append({
|
||||||
|
'name': name, 'desc': desc, 'n_allows': n_allows,
|
||||||
|
'n_blocks': n_blocks, 'peer_count': peer_count,
|
||||||
|
'extends': extends, 'is_base': is_base, 'group': group
|
||||||
|
})
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Sort: non-base first, then by group (empty group last within non-base),
|
||||||
|
# then by name within group
|
||||||
|
rules_data.sort(key=lambda x: (
|
||||||
|
x['is_base'],
|
||||||
|
x['group'] == '' and not x['is_base'],
|
||||||
|
x['group'],
|
||||||
|
x['name']
|
||||||
|
))
|
||||||
|
|
||||||
|
for r in rules_data:
|
||||||
|
print(f"{r['name']}|{r['desc']}|{r['n_allows']}|{r['n_blocks']}|"
|
||||||
|
f"{r['peer_count']}|{r['extends']}|{r['is_base']}|{r['group']}")
|
||||||
|
|
||||||
def group_list_data(groups_dir, blocks_dir):
|
def group_list_data(groups_dir, blocks_dir):
|
||||||
"""Return group summary data in one call"""
|
"""Return group summary data in one call"""
|
||||||
import glob
|
import glob
|
||||||
|
|
@ -711,6 +822,288 @@ def parse_fw_event(line):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def peer_transfer(wg_interface):
|
||||||
|
"""Get total transfer bytes per peer"""
|
||||||
|
import subprocess
|
||||||
|
low = int(os.environ.get('ACTIVITY_TOTAL_LOW_BYTES', '1000000'))
|
||||||
|
med = int(os.environ.get('ACTIVITY_TOTAL_MED_BYTES', '10000000'))
|
||||||
|
high = int(os.environ.get('ACTIVITY_TOTAL_HIGH_BYTES', '100000000'))
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['wg', 'show', wg_interface, 'transfer'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split('\t')
|
||||||
|
if len(parts) == 3:
|
||||||
|
pubkey, rx, tx = parts
|
||||||
|
total = int(rx) + int(tx)
|
||||||
|
if total == 0: level = 'none'
|
||||||
|
elif total < low: level = 'low'
|
||||||
|
elif total < med: level = 'medium'
|
||||||
|
elif total < high: level = 'high'
|
||||||
|
else: level = 'very high'
|
||||||
|
print(f"{pubkey}|{rx}|{tx}|{level}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def peer_transfer_delta(wg_interface, cache_file):
|
||||||
|
"""Calculate current transfer rate using delta from previous sample"""
|
||||||
|
import subprocess, time
|
||||||
|
|
||||||
|
low = int(os.environ.get('ACTIVITY_CURRENT_LOW_BYTES', '10000')) # 10KB/s
|
||||||
|
med = int(os.environ.get('ACTIVITY_CURRENT_MED_BYTES', '100000')) # 100KB/s
|
||||||
|
high = int(os.environ.get('ACTIVITY_CURRENT_HIGH_BYTES', '1000000')) # 1MB/s
|
||||||
|
|
||||||
|
current = {}
|
||||||
|
now = time.time()
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['wg', 'show', wg_interface, 'transfer'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split('\t')
|
||||||
|
if len(parts) == 3:
|
||||||
|
pubkey, rx, tx = parts
|
||||||
|
current[pubkey] = {'rx': int(rx), 'tx': int(tx), 'ts': now}
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
prev = {}
|
||||||
|
if os.path.exists(cache_file):
|
||||||
|
try:
|
||||||
|
with open(cache_file) as f:
|
||||||
|
prev = json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'w') as f:
|
||||||
|
json.dump(current, f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for pubkey, data in current.items():
|
||||||
|
if pubkey in prev:
|
||||||
|
dt = data['ts'] - prev[pubkey].get('ts', data['ts'])
|
||||||
|
if dt > 0:
|
||||||
|
rx_rate = max(0, (data['rx'] - prev[pubkey]['rx']) / dt)
|
||||||
|
tx_rate = max(0, (data['tx'] - prev[pubkey]['tx']) / dt)
|
||||||
|
total = rx_rate + tx_rate
|
||||||
|
if total <= 0: level = 'idle'
|
||||||
|
elif total < low: level = 'low'
|
||||||
|
elif total < med: level = 'medium'
|
||||||
|
elif total < high: level = 'high'
|
||||||
|
else: level = 'very high'
|
||||||
|
print(f"{pubkey}|{int(rx_rate)}|{int(tx_rate)}|{level}")
|
||||||
|
else:
|
||||||
|
print(f"{pubkey}|0|0|idle")
|
||||||
|
else:
|
||||||
|
print(f"{pubkey}|0|0|unknown")
|
||||||
|
|
||||||
|
def remove_events_filtered(wg_file, fw_file, filter_name, filter_ip,
|
||||||
|
filter_fw, filter_wg, before_days):
|
||||||
|
"""Remove events with filters: by name/ip, source, or age"""
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
cutoff_ts = None
|
||||||
|
if before_days:
|
||||||
|
cutoff_ts = time.time() - (float(before_days) * 86400)
|
||||||
|
|
||||||
|
def should_remove_wg(e):
|
||||||
|
if filter_name and e.get('client') != filter_name:
|
||||||
|
return False
|
||||||
|
if cutoff_ts:
|
||||||
|
try:
|
||||||
|
ts = datetime.fromisoformat(e.get('timestamp','')).timestamp()
|
||||||
|
return ts < cutoff_ts
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def should_remove_fw(e):
|
||||||
|
if filter_ip and e.get('src_ip') != filter_ip:
|
||||||
|
return False
|
||||||
|
if cutoff_ts:
|
||||||
|
try:
|
||||||
|
ts = datetime.fromisoformat(e.get('timestamp','')).timestamp()
|
||||||
|
return ts < cutoff_ts
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
removed_wg = removed_fw = 0
|
||||||
|
|
||||||
|
if not filter_fw and os.path.exists(wg_file):
|
||||||
|
lines = []
|
||||||
|
with open(wg_file) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
if should_remove_wg(e):
|
||||||
|
removed_wg += 1
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
lines.append(line)
|
||||||
|
with open(wg_file, 'w') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
if not filter_wg and os.path.exists(fw_file):
|
||||||
|
lines = []
|
||||||
|
with open(fw_file) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
if should_remove_fw(e):
|
||||||
|
removed_fw += 1
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
lines.append(line)
|
||||||
|
with open(fw_file, 'w') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
print(f"{removed_wg}|{removed_fw}")
|
||||||
|
|
||||||
|
def _rule_resolve_internal(rules_dir, rule_name, visited=None):
|
||||||
|
"""Internal recursive resolver — returns dict, does not print"""
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
if rule_name in visited:
|
||||||
|
raise ValueError(f"Circular dependency detected: {rule_name}")
|
||||||
|
visited.add(rule_name)
|
||||||
|
|
||||||
|
rule_file = find_rule_file(rules_dir, rule_name)
|
||||||
|
with open(rule_file) as f:
|
||||||
|
rule = json.load(f)
|
||||||
|
|
||||||
|
merged = {
|
||||||
|
'allow_ips': [], 'allow_ports': [],
|
||||||
|
'block_ips': [], 'block_ports': [],
|
||||||
|
'dns_redirect': False
|
||||||
|
}
|
||||||
|
|
||||||
|
for base_name in rule.get('extends', []):
|
||||||
|
base = _rule_resolve_internal(rules_dir, base_name, visited.copy())
|
||||||
|
merged['allow_ips'] += base.get('allow_ips', [])
|
||||||
|
merged['allow_ports'] += base.get('allow_ports', [])
|
||||||
|
merged['block_ips'] += base.get('block_ips', [])
|
||||||
|
merged['block_ports'] += base.get('block_ports', [])
|
||||||
|
if base.get('dns_redirect'):
|
||||||
|
merged['dns_redirect'] = True
|
||||||
|
|
||||||
|
merged['allow_ips'] = list(dict.fromkeys(merged['allow_ips'] + rule.get('allow_ips', [])))
|
||||||
|
merged['allow_ports'] = list(dict.fromkeys(merged['allow_ports'] + rule.get('allow_ports', [])))
|
||||||
|
merged['block_ips'] = list(dict.fromkeys(merged['block_ips'] + rule.get('block_ips', [])))
|
||||||
|
merged['block_ports'] = list(dict.fromkeys(merged['block_ports'] + rule.get('block_ports', [])))
|
||||||
|
if rule.get('dns_redirect'):
|
||||||
|
merged['dns_redirect'] = True
|
||||||
|
|
||||||
|
merged['name'] = rule['name']
|
||||||
|
merged['desc'] = rule.get('desc', '')
|
||||||
|
merged['extends'] = rule.get('extends', [])
|
||||||
|
return merged
|
||||||
|
|
||||||
|
def rule_resolve(rules_dir, rule_name):
|
||||||
|
"""Resolve a rule with inheritance — prints JSON"""
|
||||||
|
try:
|
||||||
|
resolved = _rule_resolve_internal(rules_dir, rule_name)
|
||||||
|
print(json.dumps(resolved))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def rule_resolve_field(rules_dir, rule_name, field):
|
||||||
|
"""Get a single field from resolved rule — prints values one per line"""
|
||||||
|
try:
|
||||||
|
resolved = _rule_resolve_internal(rules_dir, rule_name)
|
||||||
|
val = resolved.get(field, [])
|
||||||
|
if isinstance(val, list):
|
||||||
|
for v in val:
|
||||||
|
print(v)
|
||||||
|
else:
|
||||||
|
print(val)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def rule_inspect(rules_dir, rule_name):
|
||||||
|
"""Show inheritance tree for a rule"""
|
||||||
|
try:
|
||||||
|
rule_file = find_rule_file(rules_dir, rule_name)
|
||||||
|
with open(rule_file) as f:
|
||||||
|
rule = json.load(f)
|
||||||
|
resolved = _rule_resolve_internal(rules_dir, rule_name)
|
||||||
|
has_extends = bool(rule.get('extends', []))
|
||||||
|
|
||||||
|
# Own rules
|
||||||
|
for ip in rule.get('allow_ips', []):
|
||||||
|
print(f"own|allow_ip|{ip}")
|
||||||
|
for p in rule.get('allow_ports', []):
|
||||||
|
print(f"own|allow_port|{p}")
|
||||||
|
for ip in rule.get('block_ips', []):
|
||||||
|
print(f"own|block_ip|{ip}")
|
||||||
|
for p in rule.get('block_ports', []):
|
||||||
|
print(f"own|block_port|{p}")
|
||||||
|
|
||||||
|
# DNS redirect — separate section
|
||||||
|
if rule.get('dns_redirect'):
|
||||||
|
print(f"dns|dns_redirect|true")
|
||||||
|
|
||||||
|
if has_extends:
|
||||||
|
# Inherited rules per base
|
||||||
|
for base_name in rule.get('extends', []):
|
||||||
|
base = _rule_resolve_internal(rules_dir, base_name)
|
||||||
|
for ip in base.get('allow_ips', []):
|
||||||
|
print(f"inherited:{base_name}|allow_ip|{ip}")
|
||||||
|
for p in base.get('allow_ports', []):
|
||||||
|
print(f"inherited:{base_name}|allow_port|{p}")
|
||||||
|
for ip in base.get('block_ips', []):
|
||||||
|
print(f"inherited:{base_name}|block_ip|{ip}")
|
||||||
|
for p in base.get('block_ports', []):
|
||||||
|
print(f"inherited:{base_name}|block_port|{p}")
|
||||||
|
if base.get('dns_redirect'):
|
||||||
|
print(f"inherited:{base_name}|dns_redirect|true")
|
||||||
|
|
||||||
|
# Resolved summary only when inheritance exists
|
||||||
|
has_resolved = (
|
||||||
|
resolved.get('allow_ips') or resolved.get('allow_ports') or
|
||||||
|
resolved.get('block_ips') or resolved.get('block_ports') or
|
||||||
|
resolved.get('dns_redirect')
|
||||||
|
)
|
||||||
|
if has_resolved:
|
||||||
|
for ip in resolved.get('allow_ips', []):
|
||||||
|
print(f"resolved|allow_ip|{ip}")
|
||||||
|
for p in resolved.get('allow_ports', []):
|
||||||
|
print(f"resolved|allow_port|{p}")
|
||||||
|
for ip in resolved.get('block_ips', []):
|
||||||
|
print(f"resolved|block_ip|{ip}")
|
||||||
|
for p in resolved.get('block_ports', []):
|
||||||
|
print(f"resolved|block_port|{p}")
|
||||||
|
if resolved.get('dns_redirect'):
|
||||||
|
print(f"resolved|dns_redirect|true")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def find_rule_file(rules_dir, rule_name):
|
||||||
|
"""Find rule file in rules/ or rules/base/"""
|
||||||
|
for path in [
|
||||||
|
os.path.join(rules_dir, f"{rule_name}.rule"),
|
||||||
|
os.path.join(rules_dir, "base", f"{rule_name}.rule"),
|
||||||
|
]:
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
raise FileNotFoundError(f"Rule not found: {rule_name}")
|
||||||
|
|
||||||
commands = {
|
commands = {
|
||||||
'get': lambda args: get(args[0], args[1]),
|
'get': lambda args: get(args[0], args[1]),
|
||||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||||
|
|
@ -743,6 +1136,15 @@ commands = {
|
||||||
'create_group': lambda args: create_group(args[0], args[1], args[2]),
|
'create_group': lambda args: create_group(args[0], args[1], args[2]),
|
||||||
'parse_event': lambda args: parse_event(args[0]),
|
'parse_event': lambda args: parse_event(args[0]),
|
||||||
'parse_fw_event': lambda args: parse_fw_event(args[0]),
|
'parse_fw_event': lambda args: parse_fw_event(args[0]),
|
||||||
|
'peer_transfer': lambda args: peer_transfer(args[0]),
|
||||||
|
'remove_events_filtered': lambda args: remove_events_filtered(
|
||||||
|
args[0], args[1], args[2], args[3], args[4]=='true', args[5]=='true', args[6] if len(args)>6 else ''),
|
||||||
|
'peer_transfer': lambda args: peer_transfer(args[0]),
|
||||||
|
'peer_transfer_delta': lambda args: peer_transfer_delta(args[0], args[1]),
|
||||||
|
'rule_resolve': lambda args: rule_resolve(args[0], args[1]),
|
||||||
|
'rule_resolve_field': lambda args: rule_resolve_field(args[0], args[1], args[2]),
|
||||||
|
'rule_inspect': lambda args: rule_inspect(args[0], args[1]),
|
||||||
|
'find_rule_file': lambda args: print(find_rule_file(args[0], args[1])),
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
790
core/json_helper.py.bak
Normal file
790
core/json_helper.py.bak
Normal file
|
|
@ -0,0 +1,790 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
wgctl JSON helper — called by shell functions to read/write JSON files.
|
||||||
|
Usage: json_helper.py <command> <file> [key] [value]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
DATETIME_FMT = os.environ.get('WGCTL_DATETIME_FMT', '%Y-%m-%d %H:%M')
|
||||||
|
|
||||||
|
def get(file, key):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
val = data.get(key, [])
|
||||||
|
if isinstance(val, bool):
|
||||||
|
print(str(val).lower()) # true/false not True/False
|
||||||
|
elif isinstance(val, list):
|
||||||
|
if val:
|
||||||
|
print('\n'.join(str(v) for v in val))
|
||||||
|
else:
|
||||||
|
if val:
|
||||||
|
print(val)
|
||||||
|
except:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def set_key(file, key, value):
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
if os.path.exists(file):
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Try to parse as JSON value first (for arrays/bools)
|
||||||
|
try:
|
||||||
|
data[key] = json.loads(value)
|
||||||
|
except:
|
||||||
|
data[key] = value
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def delete_key(file, key):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data.pop(key, None)
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def append(file, key, value):
|
||||||
|
try:
|
||||||
|
data = {}
|
||||||
|
if os.path.exists(file):
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if key not in data:
|
||||||
|
data[key] = []
|
||||||
|
if value not in data[key]:
|
||||||
|
data[key].append(value)
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def remove_value(file, key, value):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if key in data and value in data[key]:
|
||||||
|
data[key].remove(value)
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def cat(file):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(json.dumps(data, indent=2))
|
||||||
|
except Exception as e:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def has_key(file, key):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
sys.exit(0 if key in data else 1)
|
||||||
|
except:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def filter_values(file, key, value):
|
||||||
|
"""Remove all entries where value matches"""
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
data = {k: v for k, v in data.items() if v != value}
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def last_event(file, key, field, client):
|
||||||
|
"""Get last event field for a client"""
|
||||||
|
try:
|
||||||
|
last = None
|
||||||
|
with open(file) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
if e.get(key) == client:
|
||||||
|
last = e
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if last:
|
||||||
|
print(last.get(field, ''))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def events_for(file, ip, limit):
|
||||||
|
"""Format events for a given IP"""
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
events = []
|
||||||
|
with open(file) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
if e.get('ip') == ip:
|
||||||
|
events.append(e)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
for e in events[-int(limit):]:
|
||||||
|
ts = e.get('timestamp', '')
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(ts)
|
||||||
|
ts = dt.strftime(DATETIME_FMT)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
endpoint = e.get('endpoint', '—')
|
||||||
|
client = e.get('client', '—')
|
||||||
|
event = e.get('event', '—')
|
||||||
|
print(f' {ts} {client:<20} {endpoint:<20} {event}')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fw_events(file, filter_ip, filter_type, clients_dir, limit):
|
||||||
|
"""Format firewall drop events"""
|
||||||
|
import glob
|
||||||
|
|
||||||
|
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', '')
|
||||||
|
try:
|
||||||
|
with open(conf) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('Address'):
|
||||||
|
ip = line.split('=')[1].strip().split('/')[0]
|
||||||
|
ip_to_name[ip] = name
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
events = []
|
||||||
|
last_seen = {} # (src, dst, port, proto) -> last timestamp
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Dedup key
|
||||||
|
dst = e.get('dest_ip', '')
|
||||||
|
port = e.get('dest_port', '')
|
||||||
|
proto = e.get('ip.protocol', 0)
|
||||||
|
key = (src, dst, port, proto)
|
||||||
|
|
||||||
|
ts_str = e.get('timestamp', '')
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
ts = datetime.fromisoformat(ts_str).timestamp()
|
||||||
|
except:
|
||||||
|
ts = 0
|
||||||
|
|
||||||
|
# Protocol-aware dedup window
|
||||||
|
dedup_windows = {1: 5, 6: 30, 17: 10} # icmp=5s, tcp=30s, udp=10s
|
||||||
|
window = dedup_windows.get(proto, 10)
|
||||||
|
|
||||||
|
# Skip if same event within protocol window
|
||||||
|
if key in last_seen and (ts - last_seen[key]) < window:
|
||||||
|
continue
|
||||||
|
last_seen[key] = ts
|
||||||
|
|
||||||
|
events.append(e)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for e in events[-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))
|
||||||
|
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
|
||||||
|
|
||||||
|
print(f"{ts}|{client}|{dst_str}|{proto}")
|
||||||
|
|
||||||
|
def wg_events(file, filter_client, filter_type, limit):
|
||||||
|
"""Format WireGuard events from events.log"""
|
||||||
|
events = []
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
client = e.get('client', '')
|
||||||
|
if not client:
|
||||||
|
continue
|
||||||
|
if filter_client and client != filter_client:
|
||||||
|
continue
|
||||||
|
if filter_type and not client.startswith(filter_type + '-'):
|
||||||
|
continue
|
||||||
|
events.append(e)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for e in events[-int(limit):]:
|
||||||
|
ts = e.get('timestamp', '')
|
||||||
|
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', '—')
|
||||||
|
print(f"{ts}|{client}|{endpoint}|{event}")
|
||||||
|
|
||||||
|
def format_fw_event(line, clients_dir):
|
||||||
|
"""Format a single fw_event line"""
|
||||||
|
import glob
|
||||||
|
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', '')
|
||||||
|
try:
|
||||||
|
with open(conf) as f:
|
||||||
|
for l in f:
|
||||||
|
if l.startswith('Address'):
|
||||||
|
ip = l.split('=')[1].strip().split('/')[0]
|
||||||
|
ip_to_name[ip] = name
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
src = e.get('src_ip', '')
|
||||||
|
if not src:
|
||||||
|
return None
|
||||||
|
ts = e.get('timestamp', '')
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
dt = datetime.fromisoformat(ts)
|
||||||
|
ts = dt.strftime(DATETIME_FMT)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
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))
|
||||||
|
dst_str = f"{dst}:{port}" if port else dst
|
||||||
|
client = ip_to_name.get(src, src)
|
||||||
|
return f"{ts}|{client}|{dst_str}|{proto}"
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def format_wg_event(line):
|
||||||
|
"""Format a single wg_event line"""
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
client = e.get('client', '')
|
||||||
|
if not client:
|
||||||
|
return None
|
||||||
|
ts = e.get('timestamp', '')
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
dt = datetime.fromisoformat(ts)
|
||||||
|
ts = dt.strftime(DATETIME_FMT)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
endpoint = e.get('endpoint', '—')
|
||||||
|
event = e.get('event', '—')
|
||||||
|
return f"{ts}|{client}|{endpoint}|{event}|wg"
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def remove_events(file, identifier):
|
||||||
|
"""Remove all events for a client/ip from a JSONL file"""
|
||||||
|
try:
|
||||||
|
lines = []
|
||||||
|
with open(file) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
if e.get('client') == identifier or e.get('src_ip') == identifier:
|
||||||
|
continue
|
||||||
|
lines.append(line)
|
||||||
|
except:
|
||||||
|
lines.append(line)
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_peers=""):
|
||||||
|
"""Follow both log files and output formatted events"""
|
||||||
|
import glob, time, select
|
||||||
|
|
||||||
|
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
||||||
|
peer_filter = set(filter_peers.split(',')) if filter_peers else set()
|
||||||
|
import sys
|
||||||
|
print(f"DEBUG follow_logs: peer_filter={peer_filter}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
# Build ip->name map
|
||||||
|
ip_to_name = {}
|
||||||
|
for conf in glob.glob(f"{clients_dir}/*.conf"):
|
||||||
|
name = os.path.basename(conf).replace('.conf', '')
|
||||||
|
try:
|
||||||
|
with open(conf) as f:
|
||||||
|
for l in f:
|
||||||
|
if l.startswith('Address'):
|
||||||
|
ip = l.split('=')[1].strip().split('/')[0]
|
||||||
|
ip_to_name[ip] = name
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Open files and seek to end
|
||||||
|
files = {}
|
||||||
|
for label, path in [('fw', fw_file), ('wg', wg_file)]:
|
||||||
|
if path and os.path.exists(path):
|
||||||
|
f = open(path)
|
||||||
|
f.seek(0, 2) # seek to end
|
||||||
|
files[label] = f
|
||||||
|
|
||||||
|
dedup = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
for label, f in files.items():
|
||||||
|
line = f.readline()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if label == 'fw':
|
||||||
|
src = e.get('src_ip', '')
|
||||||
|
if not src:
|
||||||
|
continue
|
||||||
|
if filter_ip and src != filter_ip:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filter by peer names if specified
|
||||||
|
if peer_filter:
|
||||||
|
client_name = ip_to_name.get(src, '')
|
||||||
|
if client_name not in peer_filter:
|
||||||
|
continue
|
||||||
|
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))
|
||||||
|
|
||||||
|
# Dedup
|
||||||
|
key = (src, dst, port, proto_num)
|
||||||
|
windows = {1: 5, 6: 30, 17: 10}
|
||||||
|
window = windows.get(proto_num, 10)
|
||||||
|
now = time.time()
|
||||||
|
if key in dedup and (now - dedup[key]) < window:
|
||||||
|
continue
|
||||||
|
dedup[key] = now
|
||||||
|
|
||||||
|
client = ip_to_name.get(src, src)
|
||||||
|
if filter_type and not client.startswith(filter_type + '-'):
|
||||||
|
continue
|
||||||
|
dst_str = f"{dst}:{port}" if port else dst
|
||||||
|
ts = e.get('timestamp', '')[:16].replace('T', ' ')
|
||||||
|
print(f"fw|{ts}|{client}|{dst_str}|{proto}", flush=True)
|
||||||
|
|
||||||
|
elif label == 'wg':
|
||||||
|
client = e.get('client', '')
|
||||||
|
if not client:
|
||||||
|
continue
|
||||||
|
if filter_ip:
|
||||||
|
ip = ip_to_name.get(filter_ip, '')
|
||||||
|
if client != ip and client != filter_ip:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if peer_filter and client not in peer_filter:
|
||||||
|
continue
|
||||||
|
if filter_type and not client.startswith(filter_type + '-'):
|
||||||
|
continue
|
||||||
|
ts = e.get('timestamp', '')[:16].replace('T', ' ')
|
||||||
|
endpoint = e.get('endpoint', '—')
|
||||||
|
event = e.get('event', '—')
|
||||||
|
print(f"wg|{ts}|{client}|{endpoint}|{event}", flush=True)
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def count(file, key):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
val = data.get(key, [])
|
||||||
|
print(len(val) if isinstance(val, list) else 0)
|
||||||
|
except:
|
||||||
|
print(0)
|
||||||
|
|
||||||
|
def audit_fw_counts(clients_dir):
|
||||||
|
"""Return peer_name:fw_count pairs from iptables and client configs"""
|
||||||
|
import glob, subprocess
|
||||||
|
|
||||||
|
# Get iptables output once
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['iptables', '-L', 'FORWARD', '-n'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
fw_output = result.stdout
|
||||||
|
except:
|
||||||
|
fw_output = ""
|
||||||
|
|
||||||
|
# Build ip->name and count rules
|
||||||
|
for conf in glob.glob(f"{clients_dir}/*.conf"):
|
||||||
|
name = os.path.basename(conf).replace('.conf', '')
|
||||||
|
try:
|
||||||
|
with open(conf) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('Address'):
|
||||||
|
ip = line.split('=')[1].strip().split('/')[0]
|
||||||
|
count = fw_output.count(ip)
|
||||||
|
print(f"{name}:{count}")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def peer_group_map(groups_dir):
|
||||||
|
"""Return peer:group pairs for all groups"""
|
||||||
|
import glob
|
||||||
|
try:
|
||||||
|
for group_file in glob.glob(f"{groups_dir}/*.group"):
|
||||||
|
try:
|
||||||
|
with open(group_file) as f:
|
||||||
|
g = json.load(f)
|
||||||
|
name = g.get('name', '')
|
||||||
|
for peer in g.get('peers', []):
|
||||||
|
if peer:
|
||||||
|
print(f"{peer}:{name}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def peer_groups(groups_dir, peer_name):
|
||||||
|
"""Find all groups containing a peer"""
|
||||||
|
import glob
|
||||||
|
try:
|
||||||
|
for group_file in glob.glob(f"{groups_dir}/*.group"):
|
||||||
|
try:
|
||||||
|
with open(group_file) as f:
|
||||||
|
g = json.load(f)
|
||||||
|
if peer_name in g.get('peers', []):
|
||||||
|
print(g.get('name', ''))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def peer_data(clients_dir, meta_dir, events_log):
|
||||||
|
import glob
|
||||||
|
|
||||||
|
meta = {}
|
||||||
|
for f in glob.glob(f"{meta_dir}/*.meta"):
|
||||||
|
name = os.path.basename(f).replace('.meta', '')
|
||||||
|
try:
|
||||||
|
with open(f) as mf:
|
||||||
|
meta[name] = json.load(mf)
|
||||||
|
except:
|
||||||
|
meta[name] = {}
|
||||||
|
|
||||||
|
last_events = {}
|
||||||
|
try:
|
||||||
|
with open(events_log) as f:
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
e = json.loads(line.strip())
|
||||||
|
client = e.get('client', '')
|
||||||
|
if client:
|
||||||
|
last_events[client] = e
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for conf in sorted(glob.glob(f"{clients_dir}/*.conf")):
|
||||||
|
name = os.path.basename(conf).replace('.conf', '')
|
||||||
|
ip = ''
|
||||||
|
try:
|
||||||
|
with open(conf) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('Address'):
|
||||||
|
ip = line.split('=')[1].strip().split('/')[0]
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
m = meta.get(name, {})
|
||||||
|
rule = m.get('rule', '')
|
||||||
|
subtype = m.get('subtype', '')
|
||||||
|
|
||||||
|
last_event = last_events.get(name, {})
|
||||||
|
last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
|
||||||
|
last_evt = last_event.get('event', '') # fixed: was last_event
|
||||||
|
|
||||||
|
print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}")
|
||||||
|
|
||||||
|
def iso_to_ts(iso_str):
|
||||||
|
"""Convert ISO timestamp to unix timestamp"""
|
||||||
|
try:
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
dt = datetime.fromisoformat(iso_str)
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
print(int(dt.timestamp()))
|
||||||
|
except:
|
||||||
|
print(0)
|
||||||
|
|
||||||
|
def rule_list_data(rules_dir, meta_dir):
|
||||||
|
"""Return all rule data for list display in one call"""
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# Count peers per rule from meta files
|
||||||
|
rule_peer_counts = {}
|
||||||
|
for f in glob.glob(f"{meta_dir}/*.meta"):
|
||||||
|
try:
|
||||||
|
with open(f) as mf:
|
||||||
|
meta = json.load(mf)
|
||||||
|
rule = meta.get('rule', '')
|
||||||
|
if rule:
|
||||||
|
rule_peer_counts[rule] = rule_peer_counts.get(rule, 0) + 1
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Read each rule file
|
||||||
|
for rule_file in sorted(glob.glob(f"{rules_dir}/*.rule")):
|
||||||
|
try:
|
||||||
|
with open(rule_file) as f:
|
||||||
|
r = json.load(f)
|
||||||
|
name = r.get('name', '')
|
||||||
|
desc = r.get('desc', '')
|
||||||
|
n_allows = len(r.get('allow_ips', [])) + len(r.get('allow_ports', []))
|
||||||
|
n_blocks = len(r.get('block_ips', [])) + len(r.get('block_ports', []))
|
||||||
|
peer_count = rule_peer_counts.get(name, 0)
|
||||||
|
print(f"{name}|{desc}|{n_allows}|{n_blocks}|{peer_count}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def group_list_data(groups_dir, blocks_dir):
|
||||||
|
"""Return group summary data in one call"""
|
||||||
|
import glob
|
||||||
|
|
||||||
|
# Get all block files
|
||||||
|
blocked_peers = set()
|
||||||
|
for f in glob.glob(f"{blocks_dir}/*.block"):
|
||||||
|
name = os.path.basename(f).replace('.block', '')
|
||||||
|
blocked_peers.add(name)
|
||||||
|
|
||||||
|
for group_file in sorted(glob.glob(f"{groups_dir}/*.group")):
|
||||||
|
try:
|
||||||
|
with open(group_file) as f:
|
||||||
|
g = json.load(f)
|
||||||
|
name = g.get('name', '')
|
||||||
|
desc = g.get('desc', '')
|
||||||
|
peers = [p for p in g.get('peers', []) if p]
|
||||||
|
total = len(peers)
|
||||||
|
blocked = sum(1 for p in peers if p in blocked_peers)
|
||||||
|
print(f"{name}|{desc}|{total}|{blocked}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def fmt_datetime(iso_str, fmt):
|
||||||
|
"""Format ISO timestamp with given strftime format"""
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
dt = datetime.fromisoformat(iso_str)
|
||||||
|
print(dt.strftime(fmt))
|
||||||
|
except:
|
||||||
|
print(iso_str)
|
||||||
|
|
||||||
|
def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips, block_ports):
|
||||||
|
rule = {
|
||||||
|
'name': name,
|
||||||
|
'desc': desc,
|
||||||
|
'dns_redirect': dns_redirect == 'true',
|
||||||
|
'allow_ips': [x for x in allow_ips.split(',') if x] if allow_ips else [],
|
||||||
|
'block_ips': [x for x in block_ips.split(',') if x] if block_ips else [],
|
||||||
|
'block_ports': [x for x in block_ports.split(',') if x] if block_ports else [],
|
||||||
|
'allow_ports': []
|
||||||
|
}
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(rule, f, indent=2)
|
||||||
|
|
||||||
|
def cleanup_config(config_file):
|
||||||
|
"""Normalize blank lines in WireGuard config"""
|
||||||
|
import re
|
||||||
|
try:
|
||||||
|
with open(config_file) as f:
|
||||||
|
config = f.read()
|
||||||
|
config = re.sub(r'\n{3,}', '\n\n', config)
|
||||||
|
config = config.rstrip('\n') + '\n'
|
||||||
|
with open(config_file, 'w') as f:
|
||||||
|
f.write(config)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def remove_peer_block(config_file, name):
|
||||||
|
"""Remove a peer block from WireGuard config by name"""
|
||||||
|
import re
|
||||||
|
try:
|
||||||
|
with open(config_file) as f:
|
||||||
|
config = f.read()
|
||||||
|
pattern = r'\n\[Peer\]\n# ' + re.escape(name) + r'\n[^\n]+\n[^\n]+\n'
|
||||||
|
result = re.sub(pattern, '\n', config)
|
||||||
|
with open(config_file, 'w') as f:
|
||||||
|
f.write(result)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def create_group(file, name, desc):
|
||||||
|
"""Create a new group JSON file"""
|
||||||
|
try:
|
||||||
|
group = {'name': name, 'desc': desc, 'peers': []}
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(group, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def parse_event(line):
|
||||||
|
"""Parse a single JSON event line"""
|
||||||
|
try:
|
||||||
|
e = json.loads(line)
|
||||||
|
print(f"{e.get('timestamp','')}|{e.get('client','')}|{e.get('endpoint','')}|{e.get('event','')}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def parse_fw_event(line):
|
||||||
|
"""Parse a single fw_events.log JSON line"""
|
||||||
|
try:
|
||||||
|
e = json.loads(line)
|
||||||
|
ts = e.get('timestamp', '')
|
||||||
|
src = e.get('src_ip', '')
|
||||||
|
dst = e.get('dest_ip', '')
|
||||||
|
port = e.get('dest_port', '')
|
||||||
|
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
||||||
|
proto_num = e.get('ip.protocol', 0)
|
||||||
|
proto = proto_map.get(proto_num, str(proto_num))
|
||||||
|
print(f"{ts}|{src}|{dst}|{port}|{proto}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def peer_transfer(wg_interface):
|
||||||
|
"""Get transfer bytes per peer from wg show"""
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['wg', 'show', wg_interface, 'transfer'],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split('\t')
|
||||||
|
if len(parts) == 3:
|
||||||
|
pubkey, rx, tx = parts
|
||||||
|
total = int(rx) + int(tx)
|
||||||
|
if total == 0:
|
||||||
|
level = 'none'
|
||||||
|
elif total < 1_000_000:
|
||||||
|
level = 'low'
|
||||||
|
elif total < 10_000_000:
|
||||||
|
level = 'medium'
|
||||||
|
elif total < 100_000_000:
|
||||||
|
level = 'high'
|
||||||
|
else:
|
||||||
|
level = 'very high'
|
||||||
|
print(f"{pubkey}|{rx}|{tx}|{level}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
'get': lambda args: get(args[0], args[1]),
|
||||||
|
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||||
|
'delete': lambda args: delete_key(args[0], args[1]),
|
||||||
|
'append': lambda args: append(args[0], args[1], args[2]),
|
||||||
|
'remove': lambda args: remove_value(args[0], args[1], args[2]),
|
||||||
|
'cat': lambda args: cat(args[0]),
|
||||||
|
'has_key': lambda args: has_key(args[0], args[1]),
|
||||||
|
'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]),
|
||||||
|
'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]),
|
||||||
|
'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4], args[5]),
|
||||||
|
'count': lambda args: count(args[0], args[1]),
|
||||||
|
'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
|
||||||
|
'peer_group_map': lambda args: peer_group_map(args[0]),
|
||||||
|
'peer_groups': lambda args: peer_groups(args[0], args[1]),
|
||||||
|
'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
|
||||||
|
'iso_to_ts': lambda args: iso_to_ts(args[0]),
|
||||||
|
'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
|
||||||
|
'group_list_data': lambda args: group_list_data(args[0], args[1]),
|
||||||
|
'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]),
|
||||||
|
'create_rule': lambda args: create_rule(args[0], args[1], args[2], args[3], args[4], args[5], args[6]),
|
||||||
|
'cleanup_config': lambda args: cleanup_config(args[0]),
|
||||||
|
'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
|
||||||
|
'create_group': lambda args: create_group(args[0], args[1], args[2]),
|
||||||
|
'parse_event': lambda args: parse_event(args[0]),
|
||||||
|
'parse_fw_event': lambda args: parse_fw_event(args[0]),
|
||||||
|
'peer_transfer': lambda args: peer_transfer(args[0]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: json_helper.py <command> <file> [key] [value]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
args = sys.argv[2:]
|
||||||
|
|
||||||
|
if cmd not in commands:
|
||||||
|
print(f"Unknown command: {cmd}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
commands[cmd](args)
|
||||||
12
core/ui.sh
12
core/ui.sh
|
|
@ -41,6 +41,18 @@ function ui::pad() {
|
||||||
printf "%b%${pad}s" "$text" ""
|
printf "%b%${pad}s" "$text" ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ui::pad_status() {
|
||||||
|
ui::pad "${1:-}" "${2:-25}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::center() {
|
||||||
|
local text="$1" width="${2:-8}"
|
||||||
|
local len=${#text}
|
||||||
|
local pad=$(( (width - len) / 2 ))
|
||||||
|
local rpad=$(( width - len - pad ))
|
||||||
|
printf "%${pad}s%s%${rpad}s" "" "$text" ""
|
||||||
|
}
|
||||||
|
|
||||||
function ui::firewall_rule() {
|
function ui::firewall_rule() {
|
||||||
local rule="$1"
|
local rule="$1"
|
||||||
if [[ "$rule" =~ ACCEPT|DNAT ]]; then
|
if [[ "$rule" =~ ACCEPT|DNAT ]]; then
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"phone-fred": "94.63.0.129",
|
"phone-fred": "94.63.0.129",
|
||||||
"phone-helena": "148.69.46.241",
|
"phone-helena": "148.69.46.73",
|
||||||
"phone-nuno": "148.69.51.160",
|
"phone-nuno": "94.63.0.129",
|
||||||
"tablet-nuno": "148.69.202.5",
|
"tablet-nuno": "148.69.202.5",
|
||||||
"guest-zephyr": "5.13.82.5",
|
"guest-zephyr": "5.13.82.5",
|
||||||
"guest-zephyr-test": "148.69.193.47",
|
"guest-zephyr-test": "94.63.0.129",
|
||||||
"desktop-roboclean": "46.189.215.231"
|
"desktop-roboclean": "46.189.215.231"
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,15 @@ function config::on_load() {
|
||||||
# Defaults
|
# Defaults
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
# Activity thresholds
|
||||||
|
declare -g _ACTIVITY_TOTAL_LOW_BYTES="${ACTIVITY_TOTAL_LOW_BYTES:-1000000}"
|
||||||
|
declare -g _ACTIVITY_TOTAL_MED_BYTES="${ACTIVITY_TOTAL_MED_BYTES:-10000000}"
|
||||||
|
declare -g _ACTIVITY_TOTAL_HIGH_BYTES="${ACTIVITY_TOTAL_HIGH_BYTES:-100000000}"
|
||||||
|
|
||||||
|
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() {
|
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}"
|
||||||
|
|
@ -22,6 +31,7 @@ function config::_init_defaults() {
|
||||||
_WG_SUBNET="${WG_SUBNET:-10.1.0.0/16}"
|
_WG_SUBNET="${WG_SUBNET:-10.1.0.0/16}"
|
||||||
_WG_PORT="${WG_PORT:-51820}"
|
_WG_PORT="${WG_PORT:-51820}"
|
||||||
_WG_ENDPOINT="${WG_ENDPOINT:-}"
|
_WG_ENDPOINT="${WG_ENDPOINT:-}"
|
||||||
|
_WG_HANDSHAKE_CHECK_TIME_SEC="${WG_HANDSHAKE_CHECK_TIME_SEC:-180}"
|
||||||
|
|
||||||
# Derived
|
# Derived
|
||||||
_WG_CONFIG="$(ctx::wg)/${_WG_INTERFACE}.conf"
|
_WG_CONFIG="$(ctx::wg)/${_WG_INTERFACE}.conf"
|
||||||
|
|
@ -51,7 +61,10 @@ function config::load() {
|
||||||
WG_PORT) _WG_PORT="$value" ;;
|
WG_PORT) _WG_PORT="$value" ;;
|
||||||
WG_SUBNET) _WG_SUBNET="$value" ;;
|
WG_SUBNET) _WG_SUBNET="$value" ;;
|
||||||
WG_LAN) _WG_LAN="$value" ;;
|
WG_LAN) _WG_LAN="$value" ;;
|
||||||
# Add debug temporarily to config::load:
|
WG_HANDSHAKE_CHECK_TIME_SEC) _WG_HANDSHAKE_CHECK_TIME_SEC="$value" ;;
|
||||||
|
ACTIVITY_LOW_BYTES) _ACTIVITY_LOW_BYTES="$value" ;;
|
||||||
|
ACTIVITY_MED_BYTES) _ACTIVITY_MED_BYTES="$value" ;;
|
||||||
|
ACTIVITY_HIGH_BYTES) _ACTIVITY_HIGH_BYTES="$value" ;;
|
||||||
DATE_FORMAT)
|
DATE_FORMAT)
|
||||||
_FMT_DATE_FORMAT="$value"
|
_FMT_DATE_FORMAT="$value"
|
||||||
fmt::set_date_format "$value"
|
fmt::set_date_format "$value"
|
||||||
|
|
@ -109,6 +122,13 @@ function config::subnet() { echo "$_WG_SUBNET"; }
|
||||||
function config::lan() { echo "$_WG_LAN"; }
|
function config::lan() { echo "$_WG_LAN"; }
|
||||||
function config::tunnel_split() { echo "$_WG_TUNNEL_SPLIT"; }
|
function config::tunnel_split() { echo "$_WG_TUNNEL_SPLIT"; }
|
||||||
function config::tunnel_full() { echo "$_WG_TUNNEL_FULL"; }
|
function config::tunnel_full() { echo "$_WG_TUNNEL_FULL"; }
|
||||||
|
function config::handshake_time_sec() { echo "$_WG_HANDSHAKE_CHECK_TIME_SEC"; }
|
||||||
|
function config::activity_total_low() { echo "$_ACTIVITY_TOTAL_LOW_BYTES"; }
|
||||||
|
function config::activity_total_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; }
|
||||||
|
function config::activity_total_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; }
|
||||||
|
function config::activity_current_low() { echo "$_ACTIVITY_TOTAL_LOW_BYTES"; }
|
||||||
|
function config::activity_current_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; }
|
||||||
|
function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; }
|
||||||
|
|
||||||
function config::server_public_key() {
|
function config::server_public_key() {
|
||||||
cat "$_WG_SERVER_PUBLIC_KEY_FILE"
|
cat "$_WG_SERVER_PUBLIC_KEY_FILE"
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,13 @@ function monitor::events_for() {
|
||||||
json::events_for "$EVENTS_LOG" "$ip" "$limit"
|
json::events_for "$EVENTS_LOG" "$ip" "$limit"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function monitor::get_handshake_ts() {
|
||||||
|
local public_key="${1:-}"
|
||||||
|
[[ -z "$public_key" ]] && echo "0" && return
|
||||||
|
wg show "$(config::interface)" latest-handshakes 2>/dev/null \
|
||||||
|
| grep "^${public_key}" | awk '{print $2}' || echo "0"
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Endpoint Cache (for blocked clients)
|
# Endpoint Cache (for blocked clients)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,52 @@ function peers::find_by_ip() {
|
||||||
done < <(peers::all)
|
done < <(peers::all)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peers::is_connected() {
|
||||||
|
local handshake_ts="${1:-0}"
|
||||||
|
local now diff threshold
|
||||||
|
now=$(date +%s)
|
||||||
|
threshold=$(config::handshake_time_sec)
|
||||||
|
[[ "$handshake_ts" == "0" || -z "$handshake_ts" ]] && return 1
|
||||||
|
diff=$(( now - handshake_ts ))
|
||||||
|
(( diff < threshold ))
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::is_attempting() {
|
||||||
|
local last_ts="${1:-}"
|
||||||
|
[[ -z "$last_ts" ]] && return 1
|
||||||
|
local now attempt_ts diff threshold
|
||||||
|
now=$(date +%s)
|
||||||
|
threshold=$(config::handshake_time_sec)
|
||||||
|
attempt_ts=$(json::iso_to_ts "$last_ts")
|
||||||
|
[[ -z "$attempt_ts" || "$attempt_ts" == "0" ]] && return 1
|
||||||
|
diff=$(( now - attempt_ts ))
|
||||||
|
(( diff < threshold ))
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::is_online() {
|
||||||
|
local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}"
|
||||||
|
local is_blocked
|
||||||
|
peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
|
||||||
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
|
peers::is_attempting "$last_ts"
|
||||||
|
else
|
||||||
|
peers::is_connected "$handshake_ts"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::is_offline() {
|
||||||
|
local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}"
|
||||||
|
peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# function peers::is_offline() {
|
||||||
|
# local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}"
|
||||||
|
# if peers::is_online "$name" "$handshake_ts" "$last_ts"; then
|
||||||
|
# return 1
|
||||||
|
# fi
|
||||||
|
# return 0
|
||||||
|
# }
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Name + Type Parsing
|
# Name + Type Parsing
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -275,6 +321,167 @@ function peers::resolve_and_require() {
|
||||||
echo "$resolved"
|
echo "$resolved"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Display / Formatting
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function peers::format_last_seen() {
|
||||||
|
local name="${1:-}" pubkey="${2:-}" is_blocked="${3:-false}"
|
||||||
|
local last_ts="${4:-}" last_evt="${5:-}" handshake_ts="${6:-0}"
|
||||||
|
|
||||||
|
local data
|
||||||
|
data=$(peers::last_seen_data "$is_blocked" "$last_ts" "$handshake_ts")
|
||||||
|
|
||||||
|
local ts type
|
||||||
|
IFS="|" read -r ts type <<< "$data"
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
none) echo "—" ;;
|
||||||
|
dropped) echo "$(fmt::datetime_iso "$ts") (dropped)" ;;
|
||||||
|
handshake) echo "$(fmt::datetime "$ts") (handshake)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::format_status() {
|
||||||
|
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
||||||
|
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
||||||
|
|
||||||
|
local state
|
||||||
|
state=$(peers::connection_state "$is_blocked" "$is_restricted" \
|
||||||
|
"$handshake_ts" "$last_ts")
|
||||||
|
|
||||||
|
local conn_str modifier color
|
||||||
|
IFS="|" read -r conn_str modifier color <<< "$state"
|
||||||
|
|
||||||
|
local display="$conn_str"
|
||||||
|
[[ -n "$modifier" ]] && display="${conn_str} (${modifier})"
|
||||||
|
echo -e "${color}${display}\033[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::display_type() {
|
||||||
|
local type="${1:-}" subtype="${2:-}"
|
||||||
|
if config::is_guest_type "$type" && [[ -n "$subtype" && "$subtype" != "0" ]]; then
|
||||||
|
echo "guest/${subtype}"
|
||||||
|
elif config::is_guest_type "$type"; then
|
||||||
|
echo "guest"
|
||||||
|
else
|
||||||
|
echo "$type"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Connection data
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Data functions — return raw values
|
||||||
|
function peers::connection_state() {
|
||||||
|
# Returns: connected|modifier|color_code
|
||||||
|
local is_blocked="${1:-false}" is_restricted="${2:-false}"
|
||||||
|
local handshake_ts="${3:-0}" last_ts="${4:-}"
|
||||||
|
local threshold
|
||||||
|
threshold=$(config::handshake_time_sec)
|
||||||
|
local now
|
||||||
|
now=$(date +%s)
|
||||||
|
local connected=false modifier="" color
|
||||||
|
|
||||||
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
|
local attempt_ts diff
|
||||||
|
attempt_ts=$(json::iso_to_ts "${last_ts:-0}")
|
||||||
|
diff=$(( now - attempt_ts ))
|
||||||
|
(( diff < threshold )) && connected=true
|
||||||
|
modifier="blocked"
|
||||||
|
color="\033[1;31m"
|
||||||
|
elif [[ "$is_restricted" == "true" ]]; then
|
||||||
|
local diff=$(( now - handshake_ts ))
|
||||||
|
(( diff < threshold )) && connected=true
|
||||||
|
modifier="restricted"
|
||||||
|
color="\033[1;33m"
|
||||||
|
else
|
||||||
|
local diff=$(( now - handshake_ts ))
|
||||||
|
(( diff < threshold )) && connected=true
|
||||||
|
$connected && color="\033[1;32m" || color="\033[0;37m"
|
||||||
|
fi
|
||||||
|
|
||||||
|
$connected && echo "online|${modifier}|${color}" || echo "offline|${modifier}|${color}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::last_seen_data() {
|
||||||
|
# Returns: timestamp|type (dropped|handshake|none)
|
||||||
|
local is_blocked="${1:-false}" last_ts="${2:-}" handshake_ts="${3:-0}"
|
||||||
|
|
||||||
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
|
if [[ -n "$last_ts" && "$last_ts" != "0" && "$last_ts" != "null" ]]; then
|
||||||
|
echo "${last_ts}|dropped"
|
||||||
|
else
|
||||||
|
echo "|none"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ -z "$handshake_ts" || "$handshake_ts" == "0" ]]; then
|
||||||
|
echo "|none"
|
||||||
|
else
|
||||||
|
echo "${handshake_ts}|handshake"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::get_type_from_ip() {
|
||||||
|
local ip="${1:-}"
|
||||||
|
[[ -z "$ip" ]] && echo "unknown" && return 0
|
||||||
|
local type="unknown"
|
||||||
|
for t in $(config::device_types); do
|
||||||
|
local subnet
|
||||||
|
subnet=$(config::subnet_for "$t")
|
||||||
|
string::starts_with "$ip" "${subnet}." && type="$t" && break
|
||||||
|
done
|
||||||
|
echo "$type"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Activity
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Returns: level|rx|tx
|
||||||
|
function peers::activity_total() {
|
||||||
|
local pubkey="${1:-}"
|
||||||
|
json::peer_transfer "$(config::interface)" | grep "^${pubkey}" | head -1 | cut -d'|' -f2-
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns: level|rx_rate|tx_rate
|
||||||
|
function peers::activity_current() {
|
||||||
|
local pubkey="${1:-}"
|
||||||
|
json::peer_transfer_delta "$(config::interface)" \
|
||||||
|
"$(ctx::daemon)/transfer_cache.json" \
|
||||||
|
| grep "^${pubkey}" | head -1 | cut -d'|' -f2-
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::format_activity_total() {
|
||||||
|
local pubkey="${1:-}"
|
||||||
|
local data
|
||||||
|
data=$(peers::activity_total "$pubkey")
|
||||||
|
[[ -z "$data" ]] && echo "—" && return 0
|
||||||
|
local level rx tx rx_hr tx_hr
|
||||||
|
IFS="|" read -r rx tx level <<< "$data"
|
||||||
|
rx_hr=$(numfmt --to=iec "${rx:-0}" 2>/dev/null || echo "0B")
|
||||||
|
tx_hr=$(numfmt --to=iec "${tx:-0}" 2>/dev/null || echo "0B")
|
||||||
|
echo "${level:-none} (↓${rx_hr} ↑${tx_hr})"
|
||||||
|
}
|
||||||
|
|
||||||
|
function peers::format_activity_current() {
|
||||||
|
local pubkey="${1:-}"
|
||||||
|
local data
|
||||||
|
data=$(peers::activity_current "$pubkey")
|
||||||
|
[[ -z "$data" ]] && echo "—" && return 0
|
||||||
|
local level rx_rate tx_rate rx_hr tx_hr
|
||||||
|
IFS="|" read -r rx_rate tx_rate level <<< "$data"
|
||||||
|
[[ "$level" == "unknown" ]] && echo "sampling..." && return 0
|
||||||
|
local rx_hr tx_hr
|
||||||
|
rx_hr=$(numfmt --to=iec "${rx_rate:-0}" 2>/dev/null || echo "${rx_rate:-0}")
|
||||||
|
tx_hr=$(numfmt --to=iec "${tx_rate:-0}" 2>/dev/null || echo "${tx_rate:-0}")
|
||||||
|
echo "${level} (↓${rx_hr}B/s ↑${tx_hr}B/s)"
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Helpers - Meta File
|
# Helpers - Meta File
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,23 @@
|
||||||
# Rule File Parsing
|
# Rule File Parsing
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
function rule::is_base() {
|
||||||
|
local name="${1:-}"
|
||||||
|
[[ -f "$(ctx::rules::base)/${name}.rule" ]]
|
||||||
|
}
|
||||||
|
|
||||||
function rule::exists() {
|
function rule::exists() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
[[ -f "$(ctx::rule::path "${name}.rule")" ]]
|
[[ -f "$(rule::path "${name}")" ]] || \
|
||||||
|
[[ -f "$(ctx::rules::base)/${name}.rule" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function rule::require_assignable() {
|
||||||
|
local name="${1:-}"
|
||||||
|
if rule::is_base "$name"; then
|
||||||
|
log::error "Cannot assign base rule '${name}' — base rules cannot be assigned directly"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::require_exists() {
|
function rule::require_exists() {
|
||||||
|
|
@ -19,12 +33,22 @@ function rule::require_exists() {
|
||||||
|
|
||||||
function rule::get() {
|
function rule::get() {
|
||||||
local name="${1:-}" key="${2:-}"
|
local name="${1:-}" key="${2:-}"
|
||||||
json::get "$(ctx::rule::path "${name}.rule")" "$key"
|
json::rule_resolve_field "$(ctx::rules)" "$name" "$key"
|
||||||
|
}
|
||||||
|
|
||||||
|
function rule::get_resolved() {
|
||||||
|
local name="${1:-}"
|
||||||
|
json::rule_resolve "$(ctx::rules)" "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
function rule::path() {
|
||||||
|
local name="${1:-}"
|
||||||
|
json::find_rule_file "$(ctx::rules)" "$name"
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::get_all() {
|
function rule::get_all() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
cat "$(ctx::rule::path "${name}.rule")"
|
rule::get_resolved "$name"
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::is_applied() {
|
function rule::is_applied() {
|
||||||
|
|
|
||||||
1
wgctl
1
wgctl
|
|
@ -50,7 +50,6 @@ declare -A CMD_ALIASES=(
|
||||||
[disable]=service
|
[disable]=service
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Dispatch
|
# Dispatch
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue