Compare commits

..

No commits in common. "9c11152682ca4859ec2fd288dade9d728e67d936" and "d26e67b94044cbad5ee3219bd6cf5f53949cd5f9" have entirely different histories.

10 changed files with 29 additions and 283 deletions

View file

@ -6,8 +6,6 @@
# ============================================ # ============================================
function cmd::activity::on_load() { function cmd::activity::on_load() {
command::mixin json_output
load_module net load_module net
flag::register --peer flag::register --peer
@ -19,7 +17,7 @@ function cmd::activity::on_load() {
flag::register --drop flag::register --drop
flag::register --external flag::register --external
flag::exclusive --accept --drop command::mixin json_output
} }
# ============================================ # ============================================
@ -190,50 +188,27 @@ function cmd::activity::run() {
# ── Accept dest inline renderer ── # ── Accept dest inline renderer ──
_render_peer_accept_dests() { _render_peer_accept_dests() {
local peer_name="$1" local peer_name="$1"
local keys="${_ACCEPT_DEST_KEYS[$peer_name]:-}" local keys="${_ACCEPT_DEST_KEYS[$peer_name]:-}"
[[ -z "$keys" ]] && return 0 [[ -z "$keys" ]] && return 0
for d_key in $keys; do for d_key in $keys; do
local dest_stats="${_ACCEPT_DEST[$d_key]:-}" local dest_stats="${_ACCEPT_DEST[$d_key]:-}"
[[ -z "$dest_stats" ]] && continue [[ -z "$dest_stats" ]] && continue
local d_bytes_orig d_bytes_reply d_count local d_bytes_orig d_bytes_reply d_count
IFS='|' read -r d_bytes_orig d_bytes_reply d_count <<< "$dest_stats" IFS='|' read -r d_bytes_orig d_bytes_reply d_count <<< "$dest_stats"
local rest_key="${d_key#${peer_name}:}" local rest_key="${d_key#${peer_name}:}"
local d_ip="${rest_key%%:*}" local d_ip="${rest_key%%:*}"
local pp="${rest_key#*:}" local pp="${rest_key#*:}"
local d_port="${pp%%:*}" local d_port="${pp%%:*}"
local d_proto="${pp##*:}" local d_proto="${pp##*:}"
local spec="${d_ip}:${d_port}:${d_proto}" local dest_display
local dest_display="${_DEST_RESOLVE_CACHE[$spec]:-${d_ip}:${d_port}/${d_proto}}" dest_display=$(resolve::dest "$d_ip" "$d_port" "$d_proto" 2>/dev/null \
ui::activity::accept_dest_row \ || echo "${d_ip}:${d_port}/${d_proto}")
"$dest_display" "$d_bytes_orig" "$d_bytes_reply" \ ui::activity::accept_dest_row \
"$d_count" "$drops_col" "$w_count" "$dest_display" "$d_bytes_orig" "$d_bytes_reply" \
done "$d_count" "$drops_col" "$w_count"
}
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 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 first_peer=true skip_peer=false current_name=""
local -a rendered_peers=() local -a rendered_peers=()

View file

@ -5,8 +5,6 @@
# ============================================ # ============================================
function cmd::list::on_load() { function cmd::list::on_load() {
command::mixin json_output
load_module identity load_module identity
load_module ui load_module ui
@ -21,9 +19,7 @@ function cmd::list::on_load() {
flag::register --allowed flag::register --allowed
flag::register --detailed flag::register --detailed
flag::register --name flag::register --name
command::mixin json_output
# Mutually exclusive filter groups
flag::exclusive --online --offline --blocked --restricted --allowed
} }
# ============================================ # ============================================

View file

@ -23,8 +23,6 @@ function cmd::logs::on_load() {
flag::register --ascending flag::register --ascending
flag::register --descending flag::register --descending
flag::register --resolved flag::register --resolved
flag::exclusive --ascending --descending
} }
function cmd::logs::help() { function cmd::logs::help() {
@ -163,11 +161,6 @@ function cmd::logs::show() {
[[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1 [[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1
fi fi
if $fw_only && $wg_only; then
fw_only=false
wg_only=false
fi
if $follow; then if $follow; then
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only" cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
return return

View file

@ -8,7 +8,6 @@ declare -A _LOADED_COMMANDS=()
readonly _COMMAND_NAMESPACE="cmd" readonly _COMMAND_NAMESPACE="cmd"
readonly _COMMAND_AUTO_LOAD_HOOK="on_load" readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
_CURRENT_LOADING_CMD=""
# ============================================ # ============================================
# Helpers # Helpers
@ -37,59 +36,15 @@ function command::exists() { command::has_function "$1" run; }
# Runner # 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() { function command::run() {
local cmd="$1" local cmd="$1"
shift shift
command::_reset_mixin_state command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
# Build default args from config local -a args=("$@")
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 command::_preprocess_flags args
echo "DEBUG cmd=$cmd groups='$groups' defs='${default_args[*]}' user='${user_args[*]:-}'" >&2
local fn local fn
fn=$(command::fn "$cmd" run) fn=$(command::fn "$cmd" run)
core::call_function "$fn" ${args[@]+"${args[@]}"} core::call_function "$fn" ${args[@]+"${args[@]}"}
@ -122,9 +77,7 @@ function load_command() {
source "$path" source "$path"
_LOADED_COMMANDS["$name"]=1 _LOADED_COMMANDS["$name"]=1
_CURRENT_LOADING_CMD="$name"
core::call_if_exists "$(command::fn "$name" on_load)" core::call_if_exists "$(command::fn "$name" on_load)"
_CURRENT_LOADING_CMD=""
return 0 return 0
} }

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# core/command_mixins.sh # core/command_mixins.sh
# Mixin infrastructure — loads mixin files and provides command::mixin / flag::exclusive # Mixin infrastructure — loads mixin files and provides command::mixin
# ============================================ # ============================================
# Active mixin tracking (per-process) # Active mixin tracking (per-process)
@ -109,87 +109,3 @@ function command::_preprocess_flags() {
_args_ref=() _args_ref=()
fi 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
}

View file

@ -157,7 +157,6 @@ function json::endpoint_cache_get() { python3 "$JSON_HELPER" endpoint_cach
# Accept Events # Accept Events
function json::accept_events() { python3 "$JSON_HELPER" accept_events "$@" </dev/null; } function json::accept_events() { python3 "$JSON_HELPER" accept_events "$@" </dev/null; }
function json::accept_aggregate() { python3 "$JSON_HELPER" accept_aggregate "$@" </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() { function json::peer_transfer() {
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \ ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \

View file

@ -1607,23 +1607,6 @@ def config_load(file):
emit('ACTIVITY_CURRENT_LOW_BYTES', acur.get('low')) emit('ACTIVITY_CURRENT_LOW_BYTES', acur.get('low'))
emit('ACTIVITY_CURRENT_MED_BYTES', acur.get('medium')) emit('ACTIVITY_CURRENT_MED_BYTES', acur.get('medium'))
emit('ACTIVITY_CURRENT_HIGH_BYTES', acur.get('high')) 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: except Exception as e:
print(f"Error: {e}", file=sys.stderr) print(f"Error: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -1838,60 +1821,6 @@ def endpoint_cache_get(cache_file, peer):
except Exception: except Exception:
print('') 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): def _net_read(file):
@ -2309,7 +2238,6 @@ commands = {
args[3] if len(args) > 3 else '', args[3] if len(args) > 3 else '',
args[4] if len(args) > 4 else '', args[4] if len(args) > 4 else '',
args[5] if len(args) > 5 else '0'), args[5] if len(args) > 5 else '0'),
'batch_resolve_dest': lambda args: batch_resolve_dest(args[0], args[1], *args[2:]),
} }
# ── Main ───────────────────────────────────────────────────────────────────── # ── Main ─────────────────────────────────────────────────────────────────────

View file

@ -22,9 +22,6 @@ declare -g _ACTIVITY_CURRENT_LOW_BYTES="${ACTIVITY_CURRENT_LOW_BYTES:-1000000}"
declare -g _ACTIVITY_CURRENT_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}" declare -g _ACTIVITY_CURRENT_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}"
declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}" declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}"
declare -gA _COMMAND_DEFAULTS=()
declare -gA _COMMAND_ALIASES=()
function config::_init_defaults() { function config::_init_defaults() {
_WG_INTERFACE="wg0" _WG_INTERFACE="wg0"
_WG_DNS="10.0.0.103" _WG_DNS="10.0.0.103"
@ -92,14 +89,6 @@ function config::_load_json() {
ACTIVITY_CURRENT_LOW_BYTES) _ACTIVITY_CURRENT_LOW_BYTES="$value" ;; ACTIVITY_CURRENT_LOW_BYTES) _ACTIVITY_CURRENT_LOW_BYTES="$value" ;;
ACTIVITY_CURRENT_MED_BYTES) _ACTIVITY_CURRENT_MED_BYTES="$value" ;; ACTIVITY_CURRENT_MED_BYTES) _ACTIVITY_CURRENT_MED_BYTES="$value" ;;
ACTIVITY_CURRENT_HIGH_BYTES) _ACTIVITY_CURRENT_HIGH_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 esac
done < <(json::config_load "$file" 2>/dev/null) done < <(json::config_load "$file" 2>/dev/null)
} }

7
wgctl
View file

@ -43,6 +43,7 @@ declare -A CMD_ALIASES=(
[del]=remove [del]=remove
[delete]=remove [delete]=remove
[mv]=rename [mv]=rename
[ls]=list
[show]=list [show]=list
[monitor]=watch [monitor]=watch
[ban]=block [ban]=block
@ -52,6 +53,7 @@ declare -A CMD_ALIASES=(
[down]=service [down]=service
[reload]=service [reload]=service
[stat]=service [stat]=service
[log]=service
[start]=service [start]=service
[stop]=service [stop]=service
[restart]=service [restart]=service
@ -76,11 +78,6 @@ function wgctl::dispatch() {
local cmd local cmd
cmd="$(wgctl::resolve_alias "$raw_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 case "$cmd" in
help) wgctl::help; return ;; help) wgctl::help; return ;;
shell) : ;; shell) : ;;