Compare commits
5 commits
feature/ac
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4545a400b | ||
|
|
10ea174e44 | ||
|
|
9c11152682 | ||
|
|
b153f222a5 | ||
|
|
d26e67b940 |
14 changed files with 481 additions and 108 deletions
|
|
@ -6,6 +6,8 @@
|
|||
# ============================================
|
||||
|
||||
function cmd::activity::on_load() {
|
||||
command::mixin json_output
|
||||
|
||||
load_module net
|
||||
|
||||
flag::register --peer
|
||||
|
|
@ -16,8 +18,11 @@ function cmd::activity::on_load() {
|
|||
flag::register --accept
|
||||
flag::register --drop
|
||||
flag::register --external
|
||||
flag::register --ports
|
||||
flag::register --exclude-service
|
||||
flag::register --include-service
|
||||
|
||||
command::mixin json_output
|
||||
flag::exclusive --accept --drop
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -57,7 +62,8 @@ EOF
|
|||
function cmd::activity::run() {
|
||||
local filter_peer="" filter_service="" filter_ip="" filter_type=""
|
||||
local hours=24
|
||||
local accept_only=false drop_only=false external_only=false
|
||||
local accept_only=false drop_only=false external_only=false show_ports=false
|
||||
local -a exclude_services=() include_services=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
|
|
@ -69,6 +75,9 @@ function cmd::activity::run() {
|
|||
--accept) accept_only=true; shift ;;
|
||||
--drop) drop_only=true; shift ;;
|
||||
--external) external_only=true; shift ;;
|
||||
--ports) show_ports=true; shift ;;
|
||||
--exclude-service) exclude_services+=("$2"); shift 2 ;;
|
||||
--include-service) include_services+=("$2"); shift 2 ;;
|
||||
--help) cmd::activity::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
|
|
@ -94,6 +103,21 @@ function cmd::activity::run() {
|
|||
fi
|
||||
[[ -n "$filter_ip" ]] && service_ip="$filter_ip"
|
||||
|
||||
# Build final exclusion list — remove any --include-service entries
|
||||
local -a final_excludes=()
|
||||
for svc in "${exclude_services[@]:-}"; do
|
||||
local included=false
|
||||
for inc in "${include_services[@]:-}"; do
|
||||
[[ "$svc" == "$inc" ]] && included=true && break
|
||||
done
|
||||
$included || final_excludes+=("$svc")
|
||||
done
|
||||
|
||||
# Build exclude string for Python (space-separated)
|
||||
local exclude_str=""
|
||||
[[ ${#final_excludes[@]} -gt 0 ]] && \
|
||||
exclude_str=$(IFS=' '; echo "${final_excludes[*]}")
|
||||
|
||||
# ── Fetch data ──
|
||||
local data=""
|
||||
if ! $accept_only; then
|
||||
|
|
@ -101,7 +125,7 @@ function cmd::activity::run() {
|
|||
"$(ctx::fw_events_log)" "$(ctx::events_log)" \
|
||||
"$(config::interface)" "$(ctx::net)" \
|
||||
"$(ctx::clients)" "$(ctx::meta)" \
|
||||
"$hours" "$filter_peer" "$service_ip" 2>/dev/null)
|
||||
"$hours" "$filter_peer" "$service_ip" "$exclude_str" 2>/dev/null)
|
||||
fi
|
||||
|
||||
local accept_data=""
|
||||
|
|
@ -112,7 +136,7 @@ function cmd::activity::run() {
|
|||
[[ -f "$(ctx::accept_events_log)" ]] && \
|
||||
accept_data=$(json::accept_aggregate \
|
||||
"$(ctx::accept_events_log)" "$(ctx::net)" "$(ctx::clients)" \
|
||||
"$since_arg" "$filter_peer" "$ext_flag" 2>/dev/null)
|
||||
"$since_arg" "$filter_peer" "$ext_flag" "$exclude_str" 2>/dev/null)
|
||||
fi
|
||||
|
||||
[[ -z "$data" && -z "$accept_data" ]] && \
|
||||
|
|
@ -188,27 +212,60 @@ function cmd::activity::run() {
|
|||
|
||||
# ── Accept dest inline renderer ──
|
||||
_render_peer_accept_dests() {
|
||||
local peer_name="$1"
|
||||
local keys="${_ACCEPT_DEST_KEYS[$peer_name]:-}"
|
||||
[[ -z "$keys" ]] && return 0
|
||||
for d_key in $keys; do
|
||||
local dest_stats="${_ACCEPT_DEST[$d_key]:-}"
|
||||
[[ -z "$dest_stats" ]] && continue
|
||||
local d_bytes_orig d_bytes_reply d_count
|
||||
IFS='|' read -r d_bytes_orig d_bytes_reply d_count <<< "$dest_stats"
|
||||
local rest_key="${d_key#${peer_name}:}"
|
||||
local d_ip="${rest_key%%:*}"
|
||||
local pp="${rest_key#*:}"
|
||||
local d_port="${pp%%:*}"
|
||||
local d_proto="${pp##*:}"
|
||||
local dest_display
|
||||
dest_display=$(resolve::dest "$d_ip" "$d_port" "$d_proto" 2>/dev/null \
|
||||
|| echo "${d_ip}:${d_port}/${d_proto}")
|
||||
ui::activity::accept_dest_row \
|
||||
"$dest_display" "$d_bytes_orig" "$d_bytes_reply" \
|
||||
"$d_count" "$drops_col" "$w_count"
|
||||
local peer_name="$1"
|
||||
local keys="${_ACCEPT_DEST_KEYS[$peer_name]:-}"
|
||||
[[ -z "$keys" ]] && return 0
|
||||
for d_key in $keys; do
|
||||
local dest_stats="${_ACCEPT_DEST[$d_key]:-}"
|
||||
[[ -z "$dest_stats" ]] && continue
|
||||
local d_bytes_orig d_bytes_reply d_count
|
||||
IFS='|' read -r d_bytes_orig d_bytes_reply d_count <<< "$dest_stats"
|
||||
local rest_key="${d_key#${peer_name}:}"
|
||||
local d_ip="${rest_key%%:*}"
|
||||
local pp="${rest_key#*:}"
|
||||
local d_port="${pp%%:*}"
|
||||
local d_proto="${pp##*:}"
|
||||
local spec="${d_ip}:${d_port}:${d_proto}"
|
||||
local dest_display
|
||||
local raw_suffix=""
|
||||
local resolved="${_DEST_RESOLVE_CACHE[$spec]:-${d_ip}:${d_port}/${d_proto}}"
|
||||
local dest_display="$resolved"
|
||||
if [[ "$show_ports" == "true" && "$resolved" != "${d_ip}:"* && "$resolved" != "${d_ip} "* ]]; then
|
||||
if [[ -n "$d_port" && "$d_port" != "0" ]]; then
|
||||
dest_display=$(printf "%s \033[2m(%s:%s)\033[0m" "$resolved" "$d_ip" "$d_port")
|
||||
else
|
||||
dest_display=$(printf "%s \033[2m(%s)\033[0m" "$resolved" "$d_ip")
|
||||
fi
|
||||
fi
|
||||
ui::activity::accept_dest_row \
|
||||
"$dest_display" "$d_bytes_orig" "$d_bytes_reply" \
|
||||
"$d_count" "$drops_col" "$w_count"
|
||||
done
|
||||
}
|
||||
|
||||
declare -gA _DEST_RESOLVE_CACHE=()
|
||||
local -a _dest_specs=()
|
||||
for _dk in "${!_ACCEPT_DEST[@]}"; do
|
||||
# key format: peer:ip:port:proto — strip peer prefix
|
||||
local _rest="${_dk#*:}"
|
||||
local _dip="${_rest%%:*}"
|
||||
local _pp="${_rest#*:}"
|
||||
local _dport="${_pp%%:*}"
|
||||
local _dproto="${_pp##*:}"
|
||||
local _spec="${_dip}:${_dport}:${_dproto}"
|
||||
# Deduplicate
|
||||
local _found=false
|
||||
for _s in "${_dest_specs[@]:-}"; do
|
||||
[[ "$_s" == "$_spec" ]] && _found=true && break
|
||||
done
|
||||
}
|
||||
$_found || _dest_specs+=("$_spec")
|
||||
done
|
||||
|
||||
if [[ ${#_dest_specs[@]} -gt 0 ]]; then
|
||||
while IFS='|' read -r _spec _display; do
|
||||
[[ -n "$_spec" ]] && _DEST_RESOLVE_CACHE["$_spec"]="$_display"
|
||||
done < <(json::batch_resolve_dest "${_dest_specs[@]}" 2>/dev/null)
|
||||
fi
|
||||
|
||||
local first_peer=true skip_peer=false current_name=""
|
||||
local -a rendered_peers=()
|
||||
|
|
@ -264,14 +321,23 @@ function cmd::activity::run() {
|
|||
;;
|
||||
|
||||
service)
|
||||
$skip_peer && continue
|
||||
$accept_only && continue
|
||||
local peer dest_display drop_count
|
||||
IFS='|' read -r peer dest_display drop_count <<< "$rest"
|
||||
local peer dest_display dst_ip dst_port proto drop_count
|
||||
IFS='|' read -r peer dest_display dst_ip dst_port proto drop_count <<< "$rest"
|
||||
# Build dim suffix if --ports
|
||||
local svc_display="$dest_display"
|
||||
if [[ "$show_ports" == "true" && -n "$dst_ip" ]]; then
|
||||
if [[ -n "$dst_port" ]]; then
|
||||
svc_display=$(printf "%s \033[2m(%s:%s)\033[0m" \
|
||||
"$dest_display" "$dst_ip" "$dst_port")
|
||||
else
|
||||
svc_display=$(printf "%s \033[2m(%s)\033[0m" \
|
||||
"$dest_display" "$dst_ip")
|
||||
fi
|
||||
fi
|
||||
local svc_drop_word="drops"
|
||||
[[ "$drop_count" -eq 1 ]] && svc_drop_word="drop"
|
||||
ui::activity::service_row \
|
||||
"$dest_display" "$drop_count" "$svc_drop_word" "$drops_col" "$w_count"
|
||||
$accept_only || ui::activity::service_row \
|
||||
"$svc_display" "$drop_count" "$svc_drop_word" "$drops_col" "$w_count"
|
||||
;;
|
||||
esac
|
||||
done <<< "$data"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
# ============================================
|
||||
|
||||
function cmd::list::on_load() {
|
||||
command::mixin json_output
|
||||
|
||||
load_module identity
|
||||
load_module ui
|
||||
|
||||
|
|
@ -19,7 +21,9 @@ function cmd::list::on_load() {
|
|||
flag::register --allowed
|
||||
flag::register --detailed
|
||||
flag::register --name
|
||||
command::mixin json_output
|
||||
|
||||
# Mutually exclusive filter groups
|
||||
flag::exclusive --online --offline --blocked --restricted --allowed
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ function cmd::logs::on_load() {
|
|||
flag::register --ascending
|
||||
flag::register --descending
|
||||
flag::register --resolved
|
||||
|
||||
flag::exclusive --ascending --descending
|
||||
}
|
||||
|
||||
function cmd::logs::help() {
|
||||
|
|
@ -161,6 +163,11 @@ function cmd::logs::show() {
|
|||
[[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1
|
||||
fi
|
||||
|
||||
if $fw_only && $wg_only; then
|
||||
fw_only=false
|
||||
wg_only=false
|
||||
fi
|
||||
|
||||
if $follow; then
|
||||
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
|
||||
return
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ declare -A _LOADED_COMMANDS=()
|
|||
|
||||
readonly _COMMAND_NAMESPACE="cmd"
|
||||
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
|
||||
# ============================================
|
||||
# Helpers
|
||||
|
|
@ -36,13 +37,57 @@ function command::exists() { command::has_function "$1" run; }
|
|||
# Runner
|
||||
# ============================================
|
||||
|
||||
# function command::run() {
|
||||
# local cmd="$1"
|
||||
# shift
|
||||
|
||||
# command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
||||
|
||||
# local -a args=("$@")
|
||||
# command::_preprocess_flags args
|
||||
|
||||
# local fn
|
||||
# fn=$(command::fn "$cmd" run)
|
||||
# core::call_function "$fn" ${args[@]+"${args[@]}"}
|
||||
# }
|
||||
|
||||
function command::run() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
||||
command::_reset_mixin_state
|
||||
|
||||
local -a args=("$@")
|
||||
# Build default args from config
|
||||
local -a default_args=()
|
||||
local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
|
||||
if [[ -n "$defaults" ]]; then
|
||||
read -ra default_args <<< "$defaults"
|
||||
fi
|
||||
|
||||
local -a user_args=("$@")
|
||||
[[ $# -gt 0 ]] && user_args=("$@")
|
||||
|
||||
# Resolve exclusive group conflicts — user args override defaults
|
||||
local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
||||
if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
|
||||
command::_resolve_conflicts default_args user_args "$groups"
|
||||
fi
|
||||
|
||||
local -a cleaned_defaults=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && cleaned_defaults+=("$_d")
|
||||
done
|
||||
default_args=("${cleaned_defaults[@]:-}")
|
||||
|
||||
local -a args=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && args+=("$_d")
|
||||
done
|
||||
for _u in "${user_args[@]:-}"; do
|
||||
[[ -n "$_u" ]] && args+=("$_u")
|
||||
done
|
||||
|
||||
# Preprocess mixin flags (--json, --no-color etc)
|
||||
command::_preprocess_flags args
|
||||
|
||||
local fn
|
||||
|
|
@ -77,7 +122,9 @@ function load_command() {
|
|||
source "$path"
|
||||
_LOADED_COMMANDS["$name"]=1
|
||||
|
||||
_CURRENT_LOADING_CMD="$name"
|
||||
core::call_if_exists "$(command::fn "$name" on_load)"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/command_mixins.sh
|
||||
# Mixin infrastructure — loads mixin files and provides command::mixin
|
||||
# Mixin infrastructure — loads mixin files and provides command::mixin / flag::exclusive
|
||||
|
||||
# ============================================
|
||||
# Active mixin tracking (per-process)
|
||||
|
|
@ -109,3 +109,87 @@ function command::_preprocess_flags() {
|
|||
_args_ref=()
|
||||
fi
|
||||
}
|
||||
|
||||
# command::_resolve_conflicts <defaults_nameref> <user_nameref> <groups_string>
|
||||
# Removes conflicting defaults when user provides a member of an exclusive group
|
||||
function command::_resolve_conflicts() {
|
||||
local -n _def_ref="$1"
|
||||
local -n _usr_ref="$2"
|
||||
local groups="$3"
|
||||
|
||||
[[ -z "$groups" ]] && return 0
|
||||
[[ ${#_def_ref[@]} -eq 0 ]] && return 0
|
||||
|
||||
# Work on a copy — progressively filter across all groups
|
||||
local -a working=("${_def_ref[@]}")
|
||||
|
||||
local group
|
||||
while IFS= read -r group; do
|
||||
[[ -z "$group" ]] && continue
|
||||
|
||||
local -a members=()
|
||||
IFS=',' read -ra members <<< "$group"
|
||||
|
||||
# Find which member (if any) the user passed from this group
|
||||
local user_member=""
|
||||
local member user_arg
|
||||
for member in "${members[@]}"; do
|
||||
for user_arg in "${_usr_ref[@]:-}"; do
|
||||
if [[ "$user_arg" == "$member" ]]; then
|
||||
user_member="$member"
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# No user member in this group — don't touch defaults
|
||||
[[ -z "$user_member" ]] && continue
|
||||
|
||||
# User passed a member — remove all OTHER members from defaults
|
||||
# (keep the same flag if it was already in defaults)
|
||||
local -a new_working=()
|
||||
local def_arg
|
||||
for def_arg in "${working[@]:-}"; do
|
||||
local is_other_member=false
|
||||
for member in "${members[@]}"; do
|
||||
# It's another member if it's in the group AND not the same as user's choice
|
||||
if [[ "$def_arg" == "$member" && "$def_arg" != "$user_member" ]]; then
|
||||
is_other_member=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
$is_other_member || new_working+=("$def_arg")
|
||||
done
|
||||
working=("${new_working[@]:-}")
|
||||
done < <(echo "$groups" | tr '|' '\n')
|
||||
|
||||
# Write back
|
||||
if [[ ${#working[@]} -gt 0 ]]; then
|
||||
_def_ref=("${working[@]}")
|
||||
else
|
||||
_def_ref=()
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Flag Exclusive
|
||||
# ============================================
|
||||
|
||||
declare -gA _FLAG_EXCLUSIVE_GROUPS=()
|
||||
|
||||
# flag::exclusive <flag1> <flag2> ...
|
||||
# Called from on_load — registers mutually exclusive flags for current command
|
||||
function flag::exclusive() {
|
||||
local cmd="${_CURRENT_LOADING_CMD:-}"
|
||||
[[ -z "$cmd" ]] && return 0
|
||||
|
||||
# Join flags with comma as one group
|
||||
local group
|
||||
group=$(IFS=','; echo "$*")
|
||||
|
||||
if [[ -n "${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}" ]]; then
|
||||
_FLAG_EXCLUSIVE_GROUPS["$cmd"]+="${group}|"
|
||||
else
|
||||
_FLAG_EXCLUSIVE_GROUPS["$cmd"]="${group}|"
|
||||
fi
|
||||
}
|
||||
|
|
@ -157,6 +157,7 @@ function json::endpoint_cache_get() { python3 "$JSON_HELPER" endpoint_cach
|
|||
# Accept Events
|
||||
function json::accept_events() { python3 "$JSON_HELPER" accept_events "$@" </dev/null; }
|
||||
function json::accept_aggregate() { python3 "$JSON_HELPER" accept_aggregate "$@" </dev/null; }
|
||||
function json::batch_resolve_dest() { python3 "$JSON_HELPER" batch_resolve_dest "$(ctx::net)" "$(ctx::hosts)" "$@" </dev/null; }
|
||||
|
||||
function json::peer_transfer() {
|
||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||
|
|
|
|||
|
|
@ -1607,6 +1607,23 @@ def config_load(file):
|
|||
emit('ACTIVITY_CURRENT_LOW_BYTES', acur.get('low'))
|
||||
emit('ACTIVITY_CURRENT_MED_BYTES', acur.get('medium'))
|
||||
emit('ACTIVITY_CURRENT_HIGH_BYTES', acur.get('high'))
|
||||
|
||||
# Command defaults and aliases
|
||||
# Output format:
|
||||
# CMD_DEFAULT:activity=--exclude-service pihole:dns-udp --limit 50
|
||||
# CMD_ALIAS:act=activity
|
||||
# CMD_ALIAS:a=activity
|
||||
cmds = d.get('commands', {})
|
||||
for cmd_name, cmd_cfg in cmds.items():
|
||||
if not isinstance(cmd_cfg, dict):
|
||||
continue
|
||||
defaults = cmd_cfg.get('defaults', [])
|
||||
if defaults:
|
||||
print(f"CMD_DEFAULT:{cmd_name}={' '.join(str(x) for x in defaults)}")
|
||||
aliases = cmd_cfg.get('aliases', [])
|
||||
for alias in aliases:
|
||||
print(f"CMD_ALIAS:{alias}={cmd_name}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
|
@ -1821,6 +1838,60 @@ def endpoint_cache_get(cache_file, peer):
|
|||
except Exception:
|
||||
print('')
|
||||
|
||||
def batch_resolve_dest(net_file, hosts_file, *dest_specs):
|
||||
"""
|
||||
Resolve multiple ip:port:proto specs at once.
|
||||
Input: "ip:port:proto" strings
|
||||
Output: "ip:port:proto|display_name" per line
|
||||
Uses same logic as resolve::dest bash function.
|
||||
"""
|
||||
from lib.util import load_net_data, load_hosts_data, reverse_lookup, hosts_lookup
|
||||
net_data = load_net_data(net_file)
|
||||
hosts_data = load_hosts_data(hosts_file)
|
||||
seen = set()
|
||||
|
||||
for spec in dest_specs:
|
||||
if not spec or spec in seen:
|
||||
continue
|
||||
seen.add(spec)
|
||||
|
||||
parts = spec.split(':')
|
||||
if len(parts) < 3:
|
||||
print(f"{spec}|{spec}")
|
||||
continue
|
||||
|
||||
ip = parts[0]
|
||||
port = parts[1]
|
||||
proto = parts[2]
|
||||
|
||||
# Try service name first
|
||||
svc = reverse_lookup(net_data, ip, port, proto)
|
||||
if svc and svc != ip:
|
||||
if port:
|
||||
display = f"{svc}:{proto}-{port}" if False else f"{svc}"
|
||||
# Use same format as resolve::dest: "svcname/proto" or "svcname:port"
|
||||
display = svc
|
||||
else:
|
||||
display = svc
|
||||
print(f"{spec}|{display}")
|
||||
continue
|
||||
|
||||
# Try host name
|
||||
host = hosts_lookup(hosts_data, ip)
|
||||
if host and host != ip:
|
||||
if port:
|
||||
print(f"{spec}|{host}:{port}/{proto}")
|
||||
else:
|
||||
print(f"{spec}|{host}")
|
||||
continue
|
||||
|
||||
# Raw fallback
|
||||
if port:
|
||||
print(f"{spec}|{ip}:{port}/{proto}")
|
||||
else:
|
||||
print(f"{spec}|{ip}")
|
||||
|
||||
|
||||
# ======================================================
|
||||
|
||||
def _net_read(file):
|
||||
|
|
@ -2077,7 +2148,8 @@ commands = {
|
|||
args[0], args[1], args[2], args[3], args[4],
|
||||
args[5], args[6] if len(args) > 6 else '24',
|
||||
args[7] if len(args) > 7 else '',
|
||||
args[8] if len(args) > 8 else ''),
|
||||
args[8] if len(args) > 8 else '',
|
||||
args[9] if len(args) > 9 else ''),
|
||||
# Rules
|
||||
'rule_resolve': lambda args: rule_resolve(args[0], args[1]),
|
||||
'rule_resolve_field':lambda args: rule_resolve_field(args[0], args[1], args[2]),
|
||||
|
|
@ -2231,13 +2303,15 @@ commands = {
|
|||
args[5] if len(args) > 5 else '1',
|
||||
args[6] if len(args) > 6 else '',
|
||||
args[7] if len(args) > 7 else '0',
|
||||
args[8] if len(args) > 8 else 'desc'),
|
||||
args[8] if len(args) > 8 else 'desc'),
|
||||
'accept_aggregate': lambda args: __import__('lib.accept_events',
|
||||
fromlist=['accept_aggregate']).accept_aggregate(
|
||||
args[0], args[1], args[2],
|
||||
args[3] if len(args) > 3 else '',
|
||||
args[4] if len(args) > 4 else '',
|
||||
args[5] if len(args) > 5 else '0'),
|
||||
args[5] if len(args) > 5 else '0',
|
||||
args[6] if len(args) > 6 else ''),
|
||||
'batch_resolve_dest': lambda args: batch_resolve_dest(args[0], args[1], *args[2:]),
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -131,7 +131,7 @@ def accept_events(file, filter_peer, filter_type, net_file,
|
|||
|
||||
|
||||
def accept_aggregate(file, net_file, clients_dir, since='',
|
||||
filter_peer='', external_only='0'):
|
||||
filter_peer='', external_only='0', exclude_services=''):
|
||||
"""
|
||||
Aggregate accept events per peer — total bytes, packets, top destinations.
|
||||
Used by wgctl activity to show accepted traffic alongside drops.
|
||||
|
|
@ -145,6 +145,7 @@ def accept_aggregate(file, net_file, clients_dir, since='',
|
|||
"""
|
||||
from collections import defaultdict
|
||||
from itertools import groupby
|
||||
from lib.util import load_net_data, hosts_lookup, reverse_lookup
|
||||
|
||||
since_dt = parse_since(since) if since else None
|
||||
show_external = str(external_only) == '1'
|
||||
|
|
@ -157,6 +158,14 @@ def accept_aggregate(file, net_file, clients_dir, since='',
|
|||
# dest_stats = defaultdict(lambda: {'bytes': 0, 'count': 0})
|
||||
dest_stats = defaultdict(lambda: {'bytes_orig': 0, 'bytes_reply': 0, 'count': 0})
|
||||
|
||||
# Build exclusion set — supports service names and ip:port:proto
|
||||
exclude_set = set()
|
||||
if exclude_services:
|
||||
for svc in exclude_services.split():
|
||||
exclude_set.add(svc.strip())
|
||||
|
||||
net_data = load_net_data(net_file) if (net_file and exclude_set) else {}
|
||||
|
||||
try:
|
||||
with open(file) as f:
|
||||
for line in f:
|
||||
|
|
@ -201,6 +210,9 @@ def accept_aggregate(file, net_file, clients_dir, since='',
|
|||
ps['packets_in'] += p_reply
|
||||
ps['conn_count'] += 1
|
||||
|
||||
if _is_excluded(dst_ip, dst_port, proto, exclude_set, net_data):
|
||||
continue
|
||||
|
||||
dest_key = (peer, dst_ip, dst_port, proto)
|
||||
dest_stats[dest_key]['bytes_orig'] += b_orig
|
||||
dest_stats[dest_key]['bytes_reply'] += b_reply
|
||||
|
|
@ -226,3 +238,23 @@ def accept_aggregate(file, net_file, clients_dir, since='',
|
|||
for (p, dst_ip, dst_port, proto), stats in top:
|
||||
print(f"dest|{p}|{dst_ip}|{dst_port}|{proto}|"
|
||||
f"{stats['bytes_orig']}|{stats['bytes_reply']}|{stats['count']}")
|
||||
|
||||
|
||||
def _is_excluded(ip, port, proto, exclude_set, net_data):
|
||||
if not exclude_set:
|
||||
return False
|
||||
# Check raw ip:port:proto
|
||||
if f"{ip}:{port}:{proto}" in exclude_set:
|
||||
return True
|
||||
# Check service name
|
||||
svc = reverse_lookup(net_data, ip, str(port), proto) if net_data else ''
|
||||
if svc and svc in exclude_set:
|
||||
return True
|
||||
# Check service:proto format (e.g. "pihole:dns-udp" -> "pihole" + "dns-udp")
|
||||
if svc:
|
||||
for excl in exclude_set:
|
||||
if ':' in excl:
|
||||
excl_svc, excl_port = excl.rsplit(':', 1)
|
||||
if excl_svc == svc and excl_port in (f"{proto}-{port}", f"dns-{proto}"):
|
||||
return True
|
||||
return False
|
||||
|
|
@ -19,18 +19,24 @@ from lib.util import (
|
|||
|
||||
def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||
clients_dir, meta_dir, hours, filter_peer,
|
||||
filter_service_ip):
|
||||
filter_service_ip, exclude_services=''):
|
||||
"""
|
||||
Aggregate activity data for wgctl activity.
|
||||
Output:
|
||||
peer|name|rx_bytes|tx_bytes|drop_count
|
||||
service|peer_name|dest_display|drop_count
|
||||
service|peer_name|dest_display|dst_ip|dst_port|proto|drop_count
|
||||
"""
|
||||
hours = int(hours) if hours else 24
|
||||
cutoff = None
|
||||
if hours > 0:
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||
|
||||
# Build exclusion set
|
||||
exclude_set = set()
|
||||
if exclude_services:
|
||||
for svc in exclude_services.split():
|
||||
exclude_set.add(svc.strip())
|
||||
|
||||
# Preload lookups once
|
||||
ip_to_peer = build_ip_to_name(clients_dir)
|
||||
pubkey_to_peer = build_pubkey_to_name(clients_dir)
|
||||
|
|
@ -39,6 +45,23 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
|||
def _reverse(dest_ip, dest_port, proto):
|
||||
return reverse_lookup(net_data, dest_ip, dest_port, proto)
|
||||
|
||||
def _is_excluded(ip, port, proto, svc_name):
|
||||
if not exclude_set:
|
||||
return False
|
||||
if f"{ip}:{port}:{proto}" in exclude_set:
|
||||
return True
|
||||
if svc_name and svc_name in exclude_set:
|
||||
return True
|
||||
if svc_name:
|
||||
for excl in exclude_set:
|
||||
if ':' in excl:
|
||||
excl_svc, excl_port = excl.rsplit(':', 1)
|
||||
if excl_svc == svc_name and excl_port in (
|
||||
f"{proto}-{port}", f"dns-{proto}", f"dns-udp", f"dns-tcp"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
# WireGuard transfer totals
|
||||
peer_rx = defaultdict(int)
|
||||
peer_tx = defaultdict(int)
|
||||
|
|
@ -59,6 +82,7 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
|||
pass
|
||||
|
||||
# Parse fw_events for drops
|
||||
# service_drops[peer][(dest_display, dst_ip, dst_port, proto)] = count
|
||||
peer_drops = defaultdict(int)
|
||||
service_drops = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
|
|
@ -102,8 +126,13 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
|||
svc_name = _reverse(dest_ip, dest_port, proto)
|
||||
dest_display = make_dest_display(dest_ip, dest_port, proto, svc_name)
|
||||
|
||||
if _is_excluded(dest_ip, dest_port, proto, svc_name):
|
||||
continue
|
||||
|
||||
peer_drops[peer] += 1
|
||||
service_drops[peer][dest_display] += 1
|
||||
# Key includes raw ip:port:proto for --ports support
|
||||
svc_key = (dest_display, dest_ip, dest_port, proto)
|
||||
service_drops[peer][svc_key] += 1
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
|
@ -125,5 +154,6 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
|||
print(f"peer|{peer}|{rx}|{tx}|{drops}")
|
||||
|
||||
svc_map = service_drops.get(peer, {})
|
||||
for dest_display, count in sorted(svc_map.items(), key=lambda x: -x[1]):
|
||||
print(f"service|{peer}|{dest_display}|{count}")
|
||||
for (dest_display, dst_ip, dst_port, proto), count in \
|
||||
sorted(svc_map.items(), key=lambda x: -x[1]):
|
||||
print(f"service|{peer}|{dest_display}|{dst_ip}|{dst_port}|{proto}|{count}")
|
||||
|
|
@ -22,6 +22,9 @@ 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}"
|
||||
|
||||
declare -gA _COMMAND_DEFAULTS=()
|
||||
declare -gA _COMMAND_ALIASES=()
|
||||
|
||||
function config::_init_defaults() {
|
||||
_WG_INTERFACE="wg0"
|
||||
_WG_DNS="10.0.0.103"
|
||||
|
|
@ -89,6 +92,14 @@ function config::_load_json() {
|
|||
ACTIVITY_CURRENT_LOW_BYTES) _ACTIVITY_CURRENT_LOW_BYTES="$value" ;;
|
||||
ACTIVITY_CURRENT_MED_BYTES) _ACTIVITY_CURRENT_MED_BYTES="$value" ;;
|
||||
ACTIVITY_CURRENT_HIGH_BYTES) _ACTIVITY_CURRENT_HIGH_BYTES="$value" ;;
|
||||
CMD_DEFAULT:*)
|
||||
local cmd_name="${key#CMD_DEFAULT:}"
|
||||
_COMMAND_DEFAULTS["$cmd_name"]="$value"
|
||||
;;
|
||||
CMD_ALIAS:*)
|
||||
local alias_name="${key#CMD_ALIAS:}"
|
||||
_COMMAND_ALIASES["$alias_name"]="$value"
|
||||
;;
|
||||
esac
|
||||
done < <(json::config_load "$file" 2>/dev/null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
# ui/activity.module.sh — rendering for wgctl activity
|
||||
|
||||
# ui::activity::peer_row <name_pad> <rx_pad> <tx_pad> <drops> <drop_word> <w_drops>
|
||||
function ui::activity::peer_row() {
|
||||
local name_pad="${1:-}" rx_pad="${2:-}" tx_pad="${3:-}" \
|
||||
drops="${4:-0}" drop_word="${5:-drops}" w_drops="${6:-1}"
|
||||
|
|
@ -10,39 +9,32 @@ function ui::activity::peer_row() {
|
|||
"$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word"
|
||||
}
|
||||
|
||||
# ui::activity::service_row <dest_display> <drop_count> <drop_word> <drops_col> <w_drops>
|
||||
# ── _strip_ansi <string> → visible string
|
||||
# Used for measuring visible length of strings that may contain ANSI codes
|
||||
function ui::activity::_visible_len() {
|
||||
local s="$1"
|
||||
printf "%b" "$s" | sed 's/\x1b\[[0-9;]*m//g' | wc -m | tr -d ' '
|
||||
}
|
||||
|
||||
# ui::activity::service_row
|
||||
# dest_display may contain ANSI (when --ports passes dim suffix)
|
||||
function ui::activity::service_row() {
|
||||
local dest_display="${1:-}" drop_count="${2:-0}" drop_word="${3:-drops}" \
|
||||
drops_col="${4:-30}" w_count="${5:-1}"
|
||||
|
||||
local arrow_prefix=" → "
|
||||
local prefix_bytes=${#arrow_prefix}
|
||||
local prefix_len=$(( prefix_bytes + ${#dest_display} ))
|
||||
# Measure visible length of dest (strip ANSI for correct padding)
|
||||
local dest_visible_len
|
||||
dest_visible_len=$(ui::activity::_visible_len "$dest_display")
|
||||
local prefix_len=$(( prefix_bytes + dest_visible_len ))
|
||||
local pad_n=$(( drops_col - prefix_len ))
|
||||
[[ $pad_n -lt 1 ]] && pad_n=1
|
||||
|
||||
printf " \033[0;31m→ %s%*s %${w_count}s %s\033[0m\n" \
|
||||
printf " \033[0;31m→\033[0m \033[0;31m%b\033[0m%*s \033[0;31m%${w_count}s %s\033[0m\n" \
|
||||
"$dest_display" "$pad_n" "" "$drop_count" "$drop_word"
|
||||
}
|
||||
|
||||
# Table versions (kept for future display config)
|
||||
function ui::activity::header_table() {
|
||||
printf "\n %-24s %-14s %-14s %s\n" "PEER" "↓ RX" "↑ TX" "DROPS"
|
||||
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
||||
}
|
||||
|
||||
function ui::activity::peer_row_table() {
|
||||
local name="${1:-}" rx_fmt="${2:-}" tx_fmt="${3:-}" \
|
||||
drops="${4:-0}" drop_word="${5:-drops}"
|
||||
printf " %-24s %-14s %-14s %s %s\n" \
|
||||
"$name" "↓$rx_fmt" "↑$tx_fmt" "$drops" "$drop_word"
|
||||
}
|
||||
|
||||
function ui::activity::service_row_table() {
|
||||
local dest_display="${1:-}" drop_count="${2:-0}" drop_word="${3:-drops}"
|
||||
printf " → %-30s %s %s\n" "$dest_display" "$drop_count" "$drop_word"
|
||||
}
|
||||
|
||||
function ui::activity::accept_row() {
|
||||
local name_pad="${1:-}" bytes_in="${2:-}" bytes_out="${3:-}" \
|
||||
conns="${4:-0}" w_count="${5:-4}"
|
||||
|
|
@ -57,7 +49,6 @@ function ui::activity::accept_row() {
|
|||
"$spaces" "$bytes_in" "$bytes_out" "$conns" "$conn_word"
|
||||
}
|
||||
|
||||
|
||||
function ui::activity::accept_dest_row() {
|
||||
local dest="${1:-}" bytes_orig="${2:-0}" bytes_reply="${3:-0}" \
|
||||
count="${4:-0}" drops_col="${5:-40}" w_count="${6:-4}"
|
||||
|
|
@ -67,19 +58,42 @@ function ui::activity::accept_dest_row() {
|
|||
|
||||
local arrow_prefix=" → "
|
||||
local prefix_bytes=${#arrow_prefix}
|
||||
local prefix_len=$(( prefix_bytes + ${#dest} ))
|
||||
# Measure visible length of dest (strip ANSI for correct padding)
|
||||
local dest_visible_len
|
||||
dest_visible_len=$(ui::activity::_visible_len "$dest")
|
||||
local prefix_len=$(( prefix_bytes + dest_visible_len ))
|
||||
local pad_n=$(( drops_col - prefix_len ))
|
||||
[[ $pad_n -lt 1 ]] && pad_n=1
|
||||
|
||||
# Only show bytes if non-zero
|
||||
# Build bytes display
|
||||
local bytes_display=""
|
||||
if [[ "$bytes_orig" -gt 0 || "$bytes_reply" -gt 0 ]]; then
|
||||
local bytes_display=" "
|
||||
[[ "$bytes_orig" -gt 0 ]] && bytes_display+="↓$(fmt::bytes "$bytes_orig") "
|
||||
[[ "$bytes_reply" -gt 0 ]] && bytes_display+="↑$(fmt::bytes "$bytes_reply")"
|
||||
bytes_display="${bytes_display% }" # trim trailing space
|
||||
bytes_display=" "
|
||||
[[ "$bytes_reply" -gt 0 ]] && bytes_display+="↓$(fmt::bytes "$bytes_reply") "
|
||||
[[ "$bytes_orig" -gt 0 ]] && bytes_display+="↑$(fmt::bytes "$bytes_orig")"
|
||||
bytes_display="${bytes_display% }"
|
||||
fi
|
||||
|
||||
printf " \033[0;32m→\033[0m \033[0;32m%s%*s %${w_count}s %-5s%s\033[0m\n" \
|
||||
# Use %b for dest to interpret ANSI, keep rest as %s/%d
|
||||
printf " \033[0;32m→\033[0m \033[0;32m%b\033[0m%*s \033[0;32m%${w_count}s %-5s\033[0m%s\n" \
|
||||
"$dest" "$pad_n" "" "$count" "$conn_word" "$bytes_display"
|
||||
}
|
||||
|
||||
# ── Table versions ──────────────────────────────────────
|
||||
|
||||
function ui::activity::header_table() {
|
||||
printf "\n %-24s %-14s %-14s %s\n" "PEER" "↓ RX" "↑ TX" "DROPS"
|
||||
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
||||
}
|
||||
|
||||
function ui::activity::peer_row_table() {
|
||||
local name="${1:-}" rx_fmt="${2:-}" tx_fmt="${3:-}" \
|
||||
drops="${4:-0}" drop_word="${5:-drops}"
|
||||
printf " %-24s %-14s %-14s %s %s\n" \
|
||||
"$name" "↓$rx_fmt" "↑$tx_fmt" "$drops" "$drop_word"
|
||||
}
|
||||
|
||||
function ui::activity::service_row_table() {
|
||||
local dest_display="${1:-}" drop_count="${2:-0}" drop_word="${3:-drops}"
|
||||
printf " → %-30s %s %s\n" "$dest_display" "$drop_count" "$drop_word"
|
||||
}
|
||||
7
wgctl
7
wgctl
|
|
@ -43,7 +43,6 @@ declare -A CMD_ALIASES=(
|
|||
[del]=remove
|
||||
[delete]=remove
|
||||
[mv]=rename
|
||||
[ls]=list
|
||||
[show]=list
|
||||
[monitor]=watch
|
||||
[ban]=block
|
||||
|
|
@ -53,7 +52,6 @@ declare -A CMD_ALIASES=(
|
|||
[down]=service
|
||||
[reload]=service
|
||||
[stat]=service
|
||||
[log]=service
|
||||
[start]=service
|
||||
[stop]=service
|
||||
[restart]=service
|
||||
|
|
@ -78,6 +76,11 @@ function wgctl::dispatch() {
|
|||
local cmd
|
||||
cmd="$(wgctl::resolve_alias "$raw_cmd")"
|
||||
|
||||
# Resolve config-defined aliases (from wgctl.json commands section)
|
||||
if [[ -n "${_COMMAND_ALIASES[$cmd]:-}" ]]; then
|
||||
cmd="${_COMMAND_ALIASES[$cmd]}"
|
||||
fi
|
||||
|
||||
case "$cmd" in
|
||||
help) wgctl::help; return ;;
|
||||
shell) : ;;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue