Compare commits
No commits in common. "feature/command-framework" and "master" have entirely different histories.
feature/co
...
master
72 changed files with 138 additions and 4987 deletions
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/activity/activity.sh — router only
|
||||
|
||||
function cmd::activity::on_load() {
|
||||
command::helpers "helpers.sh"
|
||||
command::define show "Activity monitor — accepted and dropped traffic" [*]
|
||||
}
|
||||
|
||||
hook::on "command:help:activity" command::help::auto
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/activity/helpers.sh
|
||||
# Implementation logic — render functions, data fetching, maps
|
||||
|
||||
# cmd::activity::_impl <filter_peer> <filter_service> <filter_ip> <filter_type>
|
||||
# <hours> <accept_only> <drop_only> <external_only>
|
||||
# <show_ports> <exclude_str>
|
||||
function cmd::activity::_impl() {
|
||||
local filter_peer="$1" filter_service="$2" filter_ip="$3" filter_type="$4"
|
||||
local hours="$5"
|
||||
local accept_only="$6" drop_only="$7" external_only="$8"
|
||||
local show_ports="$9" exclude_str="${10}"
|
||||
|
||||
if [[ -n "$filter_peer" && -n "$filter_type" ]]; then
|
||||
filter_peer=$(peers::resolve_and_require "$filter_peer" "$filter_type") || return 1
|
||||
fi
|
||||
|
||||
local service_ip=""
|
||||
if [[ -n "$filter_service" ]]; then
|
||||
service_ip=$(net::resolve "$filter_service" 2>/dev/null | head -1 | cut -d: -f1) || true
|
||||
[[ -z "$service_ip" ]] && log::error "Service not found: ${filter_service}" && return 1
|
||||
fi
|
||||
[[ -n "$filter_ip" ]] && service_ip="$filter_ip"
|
||||
|
||||
# ── Fetch data ──
|
||||
local data=""
|
||||
if [[ "$accept_only" != "true" ]]; then
|
||||
data=$(json::activity_aggregate \
|
||||
"$(ctx::fw_events_log)" "$(ctx::events_log)" \
|
||||
"$(config::interface)" "$(ctx::net)" \
|
||||
"$(ctx::clients)" "$(ctx::meta)" \
|
||||
"$hours" "$filter_peer" "$service_ip" "$exclude_str" 2>/dev/null)
|
||||
fi
|
||||
|
||||
local accept_data=""
|
||||
if [[ "$drop_only" != "true" ]]; then
|
||||
local since_arg="" ext_flag="0"
|
||||
[[ "$hours" -gt 0 ]] && since_arg="${hours}h"
|
||||
[[ "$external_only" == "true" ]] && ext_flag="1"
|
||||
[[ -f "$(ctx::accept_events_log)" ]] && \
|
||||
accept_data=$(json::accept_aggregate \
|
||||
"$(ctx::accept_events_log)" "$(ctx::net)" "$(ctx::clients)" \
|
||||
"$since_arg" "$filter_peer" "$ext_flag" "$exclude_str" 2>/dev/null)
|
||||
fi
|
||||
|
||||
[[ -z "$data" && -z "$accept_data" ]] && \
|
||||
log::wg_warning "No activity data found" && return 0
|
||||
|
||||
# ── Build accept maps ──
|
||||
declare -gA _ACCEPT_PEER=()
|
||||
declare -gA _ACCEPT_DEST_KEYS=()
|
||||
declare -gA _ACCEPT_DEST=()
|
||||
|
||||
while IFS='|' read -r type rest; do
|
||||
[[ -z "$type" ]] && continue
|
||||
case "$type" in
|
||||
peer)
|
||||
local a_name a_bi a_bo a_pi a_po a_conns
|
||||
IFS='|' read -r a_name a_bi a_bo a_pi a_po a_conns <<< "$rest"
|
||||
_ACCEPT_PEER["$a_name"]="${a_bi}|${a_bo}|${a_pi}|${a_po}|${a_conns}"
|
||||
;;
|
||||
dest)
|
||||
local d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count
|
||||
IFS='|' read -r d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count <<< "$rest"
|
||||
local d_key="${d_peer}:${d_ip}:${d_port}:${d_proto}"
|
||||
_ACCEPT_DEST["$d_key"]="${d_bytes_orig}|${d_bytes_reply}|${d_count}"
|
||||
_ACCEPT_DEST_KEYS["$d_peer"]+="${d_key} "
|
||||
;;
|
||||
esac
|
||||
done <<< "$accept_data"
|
||||
|
||||
# ── Measure column widths ──
|
||||
local w_peer=16 w_count=1
|
||||
|
||||
while IFS='|' read -r type rest; do
|
||||
case "$type" in
|
||||
peer)
|
||||
local name drops
|
||||
name=$(echo "$rest" | cut -d'|' -f1)
|
||||
drops=$(echo "$rest" | cut -d'|' -f4)
|
||||
(( ${#name} > w_peer )) && w_peer=${#name}
|
||||
(( ${#drops} > w_count )) && w_count=${#drops}
|
||||
;;
|
||||
service)
|
||||
local svc_count
|
||||
svc_count=$(echo "$rest" | cut -d'|' -f3)
|
||||
(( ${#svc_count} > w_count )) && w_count=${#svc_count}
|
||||
;;
|
||||
esac
|
||||
done <<< "$data"
|
||||
|
||||
for a_name in "${!_ACCEPT_PEER[@]}"; do
|
||||
(( ${#a_name} > w_peer )) && w_peer=${#a_name}
|
||||
local a_conns_val="${_ACCEPT_PEER[$a_name]##*|}"
|
||||
(( ${#a_conns_val} > w_count )) && w_count=${#a_conns_val}
|
||||
done
|
||||
for key in "${!_ACCEPT_DEST[@]}"; do
|
||||
local d_val="${_ACCEPT_DEST[$key]}"
|
||||
local d_count_val="${d_val##*|}"
|
||||
(( ${#d_count_val} > w_count )) && w_count=${#d_count_val}
|
||||
done
|
||||
|
||||
(( w_peer += 2 ))
|
||||
local drops_col=$(( w_peer + 30 ))
|
||||
|
||||
local hours_display="${hours}h"
|
||||
[[ "$hours" == "0" ]] && hours_display="all time"
|
||||
|
||||
log::section "Activity Monitor (last ${hours_display})"
|
||||
echo ""
|
||||
|
||||
if display::is_table "activity"; then
|
||||
cmd::activity::_render_table "$data"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Batch resolve accept dest IPs ──
|
||||
declare -gA _DEST_RESOLVE_CACHE=()
|
||||
local -a _dest_specs=()
|
||||
for _dk in "${!_ACCEPT_DEST[@]}"; do
|
||||
local _rest="${_dk#*:}"
|
||||
local _dip="${_rest%%:*}"
|
||||
local _pp="${_rest#*:}"
|
||||
local _dport="${_pp%%:*}"
|
||||
local _dproto="${_pp##*:}"
|
||||
local _spec="${_dip}:${_dport}:${_dproto}"
|
||||
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
|
||||
|
||||
# ── 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 spec="${d_ip}:${d_port}:${d_proto}"
|
||||
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
|
||||
}
|
||||
|
||||
local first_peer=true skip_peer=false current_name=""
|
||||
local -a rendered_peers=()
|
||||
|
||||
# ── Main render loop ──
|
||||
while IFS='|' read -r record_type rest; do
|
||||
case "$record_type" in
|
||||
peer)
|
||||
local name rx tx drops
|
||||
IFS='|' read -r name rx tx drops <<< "$rest"
|
||||
|
||||
[[ -n "$current_name" ]] && [[ "$drop_only" != "true" ]] && \
|
||||
_render_peer_accept_dests "$current_name"
|
||||
|
||||
skip_peer=false
|
||||
current_name="$name"
|
||||
local has_accept="${_ACCEPT_PEER[$name]:-}"
|
||||
|
||||
$first_peer || echo ""
|
||||
first_peer=false
|
||||
rendered_peers+=("$name")
|
||||
|
||||
local rx_fmt tx_fmt
|
||||
rx_fmt=$(fmt::bytes "$rx")
|
||||
tx_fmt=$(fmt::bytes "$tx")
|
||||
local name_pad rx_pad tx_pad
|
||||
name_pad=$(printf "%-${w_peer}s" "$name")
|
||||
rx_pad=$(printf "%-10s" "$rx_fmt")
|
||||
tx_pad=$(printf "%-10s" "$tx_fmt")
|
||||
|
||||
local drop_word="drops"
|
||||
[[ "$drops" -eq 1 ]] && drop_word="drop"
|
||||
|
||||
if [[ "$accept_only" == "true" ]]; then
|
||||
printf " \033[1m%s\033[0m\n" "$name_pad"
|
||||
else
|
||||
ui::activity::peer_row \
|
||||
"$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word" "$w_count"
|
||||
fi
|
||||
|
||||
if [[ -n "$has_accept" && "$drop_only" != "true" ]]; then
|
||||
local a_bi a_bo a_pi a_po a_conns
|
||||
IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$has_accept"
|
||||
ui::activity::accept_row \
|
||||
"$name_pad" \
|
||||
"$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
|
||||
"$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
|
||||
"$a_conns" "$w_count"
|
||||
fi
|
||||
;;
|
||||
|
||||
service)
|
||||
$skip_peer && continue
|
||||
[[ "$accept_only" == "true" ]] && continue
|
||||
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"
|
||||
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 \
|
||||
"$svc_display" "$drop_count" "$svc_drop_word" "$drops_col" "$w_count"
|
||||
;;
|
||||
esac
|
||||
done <<< "$data"
|
||||
|
||||
[[ -n "$current_name" ]] && [[ "$drop_only" != "true" ]] && \
|
||||
_render_peer_accept_dests "$current_name"
|
||||
|
||||
# ── Accept-only peers ──
|
||||
if [[ "$drop_only" != "true" ]]; then
|
||||
for a_name in $(echo "${!_ACCEPT_PEER[@]}" | tr ' ' '\n' | sort); do
|
||||
local already=false
|
||||
for rp in "${rendered_peers[@]:-}"; do
|
||||
[[ "$rp" == "$a_name" ]] && already=true && break
|
||||
done
|
||||
$already && continue
|
||||
|
||||
$first_peer || echo ""
|
||||
first_peer=false
|
||||
|
||||
local a_stats="${_ACCEPT_PEER[$a_name]}"
|
||||
local a_bi a_bo a_pi a_po a_conns
|
||||
IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$a_stats"
|
||||
local name_pad
|
||||
name_pad=$(printf "%-${w_peer}s" "$a_name")
|
||||
printf " \033[1m%s\033[0m\n" "$name_pad"
|
||||
ui::activity::accept_row \
|
||||
"$name_pad" \
|
||||
"$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
|
||||
"$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
|
||||
"$a_conns" "$w_count"
|
||||
_render_peer_accept_dests "$a_name"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
function cmd::activity::_render_table() {
|
||||
local data="${1:-}"
|
||||
[[ -z "$data" ]] && return 0
|
||||
ui::activity::header_table
|
||||
local skip_peer=false
|
||||
while IFS='|' read -r record_type rest; do
|
||||
case "$record_type" in
|
||||
peer)
|
||||
local name rx tx drops
|
||||
IFS='|' read -r name rx tx drops <<< "$rest"
|
||||
skip_peer=false
|
||||
local rx_fmt tx_fmt
|
||||
rx_fmt=$(fmt::bytes "$rx")
|
||||
tx_fmt=$(fmt::bytes "$tx")
|
||||
local drop_word="drops"
|
||||
[[ "$drops" -eq 1 ]] && drop_word="drop"
|
||||
ui::activity::peer_row_table "$name" "$rx_fmt" "$tx_fmt" "$drops" "$drop_word"
|
||||
;;
|
||||
service)
|
||||
$skip_peer && continue
|
||||
local peer dest_display drop_count
|
||||
IFS='|' read -r peer dest_display drop_count <<< "$rest"
|
||||
ui::activity::service_row_table "$dest_display" "$drop_count" "drops"
|
||||
;;
|
||||
esac
|
||||
done <<< "$data"
|
||||
}
|
||||
|
||||
function cmd::activity::_output_json() {
|
||||
local hours="${1:-24}"
|
||||
local data
|
||||
data=$(json::activity_aggregate \
|
||||
"$(ctx::fw_events_log)" "$(ctx::events_log)" \
|
||||
"$(config::interface)" "$(ctx::net)" \
|
||||
"$(ctx::clients)" "$(ctx::meta)" \
|
||||
"$hours" "" "" "" 2>/dev/null)
|
||||
local -a peers=()
|
||||
while IFS='|' read -r record_type rest; do
|
||||
[[ "$record_type" != "peer" ]] && continue
|
||||
local name rx tx drops
|
||||
IFS='|' read -r name rx tx drops <<< "$rest"
|
||||
peers+=("{\"name\":\"${name}\",\"rx\":${rx},\"tx\":${tx},\"drops\":${drops}}")
|
||||
done <<< "$data"
|
||||
local array
|
||||
array=$(IFS=','; echo "${peers[*]:-}")
|
||||
printf '[%s]' "$array" | json::envelope "activity" "${#peers[@]}"
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/activity/show.sh
|
||||
|
||||
function cmd::activity::show::on_load() {
|
||||
command::mixin json_output [section="Output"]
|
||||
|
||||
help::section "Filters"
|
||||
flag::define --peer value "Filter by peer name" label:name section:Filters
|
||||
flag::define --type value "Filter by device type" label:type section:Filters
|
||||
flag::define --service value "Filter by service" label:service section:Filters
|
||||
flag::define --ip value "Filter by destination IP" label:ip section:Filters
|
||||
flag::define --hours value "Hours to look back" default:24 type:int min:0 section:Filters
|
||||
flag::define --exclude-service[] "Exclude service from output" label:service section:Filters
|
||||
flag::define --include-service[] "Override excluded service" label:service section:Filters
|
||||
|
||||
help::section "Display"
|
||||
flag::define --accept bool "Show only accepted traffic" section:Display
|
||||
flag::define --drop bool "Show only firewall drops" section:Display
|
||||
flag::define --external bool "Show only external traffic" section:Display
|
||||
flag::define --ports bool "Show raw IP:port annotations" section:Display
|
||||
|
||||
flag::exclusive --accept --drop
|
||||
}
|
||||
|
||||
function cmd::activity::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local filter_peer; filter_peer=$(flag::value --peer)
|
||||
local filter_service; filter_service=$(flag::value --service)
|
||||
local filter_ip; filter_ip=$(flag::value --ip)
|
||||
local filter_type; filter_type=$(flag::value --type)
|
||||
local hours; hours=$(flag::value --hours)
|
||||
local accept_only=false drop_only=false external_only=false show_ports=false
|
||||
|
||||
flag::bool --accept && accept_only=true
|
||||
flag::bool --drop && drop_only=true
|
||||
flag::bool --external && external_only=true
|
||||
flag::bool --ports && show_ports=true
|
||||
|
||||
# Build exclusion list — remove any --include-service entries
|
||||
local -a exclude_services=() include_services=()
|
||||
while IFS= read -r svc; do
|
||||
[[ -n "$svc" ]] && exclude_services+=("$svc")
|
||||
done < <(flag::array --exclude-service)
|
||||
while IFS= read -r svc; do
|
||||
[[ -n "$svc" ]] && include_services+=("$svc")
|
||||
done < <(flag::array --include-service)
|
||||
|
||||
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
|
||||
|
||||
local exclude_str=""
|
||||
[[ ${#final_excludes[@]} -gt 0 ]] && \
|
||||
exclude_str=$(IFS=' '; echo "${final_excludes[*]}")
|
||||
|
||||
if command::json; then
|
||||
cmd::activity::_output_json "$hours"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cmd::activity::_impl \
|
||||
"$filter_peer" "$filter_service" "$filter_ip" "$filter_type" \
|
||||
"$hours" "$accept_only" "$drop_only" "$external_only" \
|
||||
"$show_ports" "$exclude_str"
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/block/block.sh — router only
|
||||
|
||||
function cmd::block::on_load() {
|
||||
command::helpers "helpers.sh"
|
||||
command::define show "Block a peer or service" [*]
|
||||
}
|
||||
|
||||
hook::on "command:help:block" command::help::auto
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/block/helpers.sh
|
||||
|
||||
# cmd::block::_impl <name> <identity> <type> <block_name> <reason>
|
||||
# <quiet> <force>
|
||||
# <ips_nameref> <subnets_nameref> <ports_nameref> <services_nameref>
|
||||
function cmd::block::_impl() {
|
||||
local name="$1" identity="$2" type="$3" block_name="$4" reason="$5"
|
||||
local quiet="$6" force="$7"
|
||||
local -n _ips_ref="$8"
|
||||
local -n _subnets_ref="$9"
|
||||
local -n _ports_ref="${10}"
|
||||
local -n _services_ref="${11}"
|
||||
|
||||
# --identity: block all peers for this identity
|
||||
if [[ -n "$identity" ]]; then
|
||||
cmd::block::_block_identity "$identity" "$quiet" \
|
||||
"${_ips_ref[@]+"${_ips_ref[@]}"}" || return 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ -z "$name" ]] && {
|
||||
log::error "Missing required flag: --name or --identity"
|
||||
return 1
|
||||
}
|
||||
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
|
||||
local client_ip
|
||||
client_ip=$(peers::get_ip "$name") || return 1
|
||||
|
||||
# Full block if no specific targets
|
||||
if [[ ${#_ips_ref[@]} -eq 0 && ${#_ports_ref[@]} -eq 0 && \
|
||||
${#_subnets_ref[@]} -eq 0 && ${#_services_ref[@]} -eq 0 ]]; then
|
||||
if peers::is_blocked "$name"; then
|
||||
log::wg_warning "Client is already blocked: ${name}"
|
||||
return 0
|
||||
fi
|
||||
monitor::update_endpoint_cache
|
||||
cmd::block::_block_all "$name" "$client_ip" "$quiet"
|
||||
cmd::block::_record_history "$name" "full" "manual" "$reason"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Specific rules
|
||||
if block::has_file "$name"; then
|
||||
local direct
|
||||
direct=$(block::is_blocked_direct "$name")
|
||||
if [[ "$direct" == "true" ]]; then
|
||||
log::wg_warning "${name} is fully blocked — unblock first to add specific rules"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local changed=false
|
||||
|
||||
for ip in "${_ips_ref[@]:-}"; do
|
||||
ip::require_valid "$ip"
|
||||
fw::block_ip "$client_ip" "$ip"
|
||||
block::add_rule "$name" "$client_ip" "ip" "${block_name:-}" "$ip"
|
||||
$quiet || log::wg_success "${ip} has been blocked for ${name}"
|
||||
done
|
||||
|
||||
for subnet in "${_subnets_ref[@]:-}"; do
|
||||
ip::require_valid "$subnet"
|
||||
fw::block_subnet "$client_ip" "$subnet"
|
||||
block::add_rule "$name" "$client_ip" "subnet" "${block_name:-}" "$subnet"
|
||||
$quiet || log::wg_success "${subnet} has been blocked for ${name}"
|
||||
done
|
||||
|
||||
for entry in "${_ports_ref[@]:-}"; do
|
||||
local b_target b_port b_proto
|
||||
IFS=":" read -r b_target b_port b_proto <<< "$entry"
|
||||
ip::require_valid "$b_target"
|
||||
fw::block_port "$client_ip" "$b_target" "$b_port" "${b_proto:-tcp}"
|
||||
block::add_rule "$name" "$client_ip" "port" "${block_name:-}" \
|
||||
"$b_target" "$b_port" "${b_proto:-tcp}"
|
||||
$quiet || log::wg_success "${b_target}:${b_port}:${b_proto:-tcp} has been blocked for ${name}"
|
||||
done
|
||||
|
||||
for svc in "${_services_ref[@]:-}"; do
|
||||
local resolved_lines=()
|
||||
mapfile -t resolved_lines < <(net::resolve "$svc" 2>/dev/null)
|
||||
if [[ ${#resolved_lines[@]} -eq 0 ]]; then
|
||||
log::error "Service not found or has no ports: ${svc}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local already_blocked=true
|
||||
for resolved in "${resolved_lines[@]}"; do
|
||||
if [[ "$resolved" == *:*:* ]]; then
|
||||
local b_ip b_port b_proto
|
||||
IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
|
||||
fw::has_block_rule "$client_ip" "$b_ip" "$b_port" "$b_proto" 2>/dev/null || \
|
||||
{ already_blocked=false; break; }
|
||||
else
|
||||
fw::has_block_rule "$client_ip" "$resolved" 2>/dev/null || \
|
||||
{ already_blocked=false; break; }
|
||||
fi
|
||||
done
|
||||
|
||||
if $already_blocked; then
|
||||
$quiet || log::wg_warning "${svc} is already blocked for ${name}"
|
||||
continue
|
||||
fi
|
||||
|
||||
for resolved in "${resolved_lines[@]}"; do
|
||||
if [[ "$resolved" == *:*:* ]]; then
|
||||
local b_ip b_port b_proto
|
||||
IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
|
||||
fw::block_port "$client_ip" "$b_ip" "$b_port" "$b_proto"
|
||||
block::add_rule "$name" "$client_ip" "port" "$svc" \
|
||||
"$b_ip" "$b_port" "$b_proto"
|
||||
else
|
||||
fw::block_ip "$client_ip" "$resolved"
|
||||
block::add_rule "$name" "$client_ip" "ip" "$svc" "$resolved"
|
||||
fi
|
||||
done
|
||||
|
||||
changed=true
|
||||
$quiet || log::wg_success "${svc} has been blocked for ${name}"
|
||||
done
|
||||
|
||||
[[ ${#_ips_ref[@]} -gt 0 || ${#_ports_ref[@]} -gt 0 || \
|
||||
${#_subnets_ref[@]} -gt 0 ]] && changed=true
|
||||
|
||||
if $changed; then
|
||||
local peer_rule
|
||||
peer_rule=$(peers::get_meta "$name" "rule")
|
||||
if [[ -n "$peer_rule" ]] && rule::exists "$peer_rule"; then
|
||||
fw::flush_peer "$client_ip"
|
||||
rule::apply "$peer_rule" "$client_ip" "$name"
|
||||
block::restore_rules_for "$name" "$client_ip"
|
||||
fi
|
||||
fi
|
||||
|
||||
local btype="specific"
|
||||
[[ ${#_services_ref[@]} -gt 0 ]] && btype="${_services_ref[0]}"
|
||||
[[ ${#_ips_ref[@]} -gt 0 ]] && btype="ip"
|
||||
[[ ${#_subnets_ref[@]} -gt 0 ]] && btype="subnet"
|
||||
[[ ${#_ports_ref[@]} -gt 0 ]] && btype="port"
|
||||
cmd::block::_record_history "$name" "$btype" "manual" "$reason"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::block::_block_identity() {
|
||||
local identity_name="${1:-}" quiet="${2:-false}"
|
||||
shift 2 || true
|
||||
|
||||
identity::require_exists "$identity_name" || return 1
|
||||
identity::require_has_peers "$identity_name" || return 1
|
||||
|
||||
local peers blocked=0 failed=0
|
||||
peers=$(identity::peers "$identity_name")
|
||||
|
||||
while IFS= read -r peer_name; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
if peers::is_blocked "$peer_name"; then
|
||||
$quiet || log::wg_warning "${peer_name} is already blocked"
|
||||
continue
|
||||
fi
|
||||
local client_ip
|
||||
client_ip=$(peers::get_ip "$peer_name") || continue
|
||||
monitor::update_endpoint_cache
|
||||
if cmd::block::_block_all "$peer_name" "$client_ip" true; then
|
||||
blocked=$(( blocked + 1 ))
|
||||
else
|
||||
failed=$(( failed + 1 ))
|
||||
fi
|
||||
done <<< "$peers"
|
||||
|
||||
log::ok "Blocked ${blocked} peer(s) for identity '${identity_name}'"
|
||||
[[ $failed -gt 0 ]] && log::warn "${failed} peer(s) failed to block"
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::block::_get_endpoint() {
|
||||
local name="$1" public_key="$2"
|
||||
local endpoint
|
||||
endpoint=$(monitor::endpoint_for_key "$public_key")
|
||||
if [[ -z "$endpoint" || "$endpoint" == "(none)" ]]; then
|
||||
endpoint=$(monitor::get_cached_endpoint "$name")
|
||||
fi
|
||||
echo "$endpoint"
|
||||
}
|
||||
|
||||
function cmd::block::_block_all() {
|
||||
local name="${1:?name required}"
|
||||
local client_ip="${2:?client_ip required}"
|
||||
local quiet="${3:-false}"
|
||||
|
||||
block::apply_full "$name" "$client_ip"
|
||||
block::set_direct "$name" "$client_ip" "true"
|
||||
|
||||
$quiet || log::wg_success "${name} has been blocked."
|
||||
}
|
||||
|
||||
function cmd::block::_record_history() {
|
||||
local name="${1:-}" block_type="${2:-full}" \
|
||||
triggered_by="${3:-manual}" reason="${4:-}"
|
||||
|
||||
local endpoint
|
||||
endpoint=$(json::peer_history_lookup "$name" 2>/dev/null || true)
|
||||
|
||||
# endpoint_cache lookup
|
||||
local ep_cache
|
||||
ep_cache=$(json::endpoint_cache_get "$(ctx::endpoint_cache)" "$name" 2>/dev/null || true)
|
||||
|
||||
json::block_history_record \
|
||||
"$(ctx::block_history)" \
|
||||
"$name" \
|
||||
"$block_type" \
|
||||
"$triggered_by" \
|
||||
"$reason" \
|
||||
"${ep_cache:-}" \
|
||||
2>/dev/null > /dev/null || true
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/block/show.sh
|
||||
|
||||
function cmd::block::show::on_load() {
|
||||
help::section "Target"
|
||||
flag::define --name value "Peer name to block" label:name section:Target
|
||||
flag::define --identity value "Block all peers in identity" label:identity section:Target
|
||||
flag::define --type value "Filter by device type" label:type section:Target
|
||||
|
||||
help::section "Rules"
|
||||
flag::define --ip[] "Block specific IP" label:ip section:Rules
|
||||
flag::define --subnet[] "Block specific subnet" label:subnet section:Rules
|
||||
flag::define --port[] "Block specific port (ip:port:proto)" label:port section:Rules
|
||||
flag::define --service[] "Block by service name" label:service section:Rules
|
||||
flag::define --block-name value "Label for this block rule" label:name section:Rules
|
||||
|
||||
help::section "Options"
|
||||
flag::define --reason value "Reason for block (recorded in history)" label:reason section:Options
|
||||
flag::define --force bool "Skip confirmation" section:Options
|
||||
flag::define --quiet bool "Suppress output" section:Options
|
||||
|
||||
flag::exclusive --name --identity
|
||||
}
|
||||
|
||||
function cmd::block::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local name; name=$(flag::value --name)
|
||||
local identity; identity=$(flag::value --identity)
|
||||
local type; type=$(flag::value --type)
|
||||
local block_name; block_name=$(flag::value --block-name)
|
||||
local reason; reason=$(flag::value --reason)
|
||||
local quiet=false force=false
|
||||
flag::bool --quiet && quiet=true
|
||||
flag::bool --force && force=true
|
||||
|
||||
# Array flags
|
||||
local -a ips=() subnets=() ports=() services=()
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && ips+=("$v"); done < <(flag::array --ip)
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && subnets+=("$v"); done < <(flag::array --subnet)
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && ports+=("$v"); done < <(flag::array --port)
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && services+=("$v"); done < <(flag::array --service)
|
||||
|
||||
# Require --name or --identity
|
||||
if [[ -z "$name" && -z "$identity" ]]; then
|
||||
log::error "Missing required flag: --name or --identity"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cmd::block::_impl \
|
||||
"$name" "$identity" "$type" "$block_name" "$reason" \
|
||||
"$quiet" "$force" \
|
||||
ips subnets ports services
|
||||
}
|
||||
|
|
@ -184,98 +184,98 @@ function cmd::config::migrate() {
|
|||
|| log::wg_success "Migration complete"
|
||||
}
|
||||
|
||||
# function config::_convert_to_json() {
|
||||
# local legacy_file="$1" output_file="$2"
|
||||
function config::_convert_to_json() {
|
||||
local legacy_file="$1" output_file="$2"
|
||||
|
||||
# # Read legacy conf into variables
|
||||
# local wg_interface="wg0" wg_endpoint="" wg_dns="10.0.0.103"
|
||||
# local wg_dns_fallback="" wg_port="51820" wg_subnet="10.1.0.0/16"
|
||||
# local wg_lan="10.0.0.0/24" wg_hs_check="300" date_format="eu"
|
||||
# Read legacy conf into variables
|
||||
local wg_interface="wg0" wg_endpoint="" wg_dns="10.0.0.103"
|
||||
local wg_dns_fallback="" wg_port="51820" wg_subnet="10.1.0.0/16"
|
||||
local wg_lan="10.0.0.0/24" wg_hs_check="300" date_format="eu"
|
||||
|
||||
# while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||
# [[ "$key" =~ ^[[:space:]]*# ]] && continue
|
||||
# [[ -z "${key// }" ]] && continue
|
||||
# key="${key// /}"
|
||||
# value="${value// /}"
|
||||
# case "$key" in
|
||||
# WG_INTERFACE) wg_interface="$value" ;;
|
||||
# WG_ENDPOINT) wg_endpoint="$value" ;;
|
||||
# WG_DNS) wg_dns="$value" ;;
|
||||
# WG_DNS_FALLBACK) wg_dns_fallback="$value" ;;
|
||||
# WG_PORT) wg_port="$value" ;;
|
||||
# WG_SUBNET) wg_subnet="$value" ;;
|
||||
# WG_LAN) wg_lan="$value" ;;
|
||||
# WG_HANDSHAKE_CHECK_TIME_SEC) wg_hs_check="$value" ;;
|
||||
# DATE_FORMAT) date_format="$value" ;;
|
||||
# esac
|
||||
# done < "$legacy_file"
|
||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${key// }" ]] && continue
|
||||
key="${key// /}"
|
||||
value="${value// /}"
|
||||
case "$key" in
|
||||
WG_INTERFACE) wg_interface="$value" ;;
|
||||
WG_ENDPOINT) wg_endpoint="$value" ;;
|
||||
WG_DNS) wg_dns="$value" ;;
|
||||
WG_DNS_FALLBACK) wg_dns_fallback="$value" ;;
|
||||
WG_PORT) wg_port="$value" ;;
|
||||
WG_SUBNET) wg_subnet="$value" ;;
|
||||
WG_LAN) wg_lan="$value" ;;
|
||||
WG_HANDSHAKE_CHECK_TIME_SEC) wg_hs_check="$value" ;;
|
||||
DATE_FORMAT) date_format="$value" ;;
|
||||
esac
|
||||
done < "$legacy_file"
|
||||
|
||||
# # Build fallback DNS array
|
||||
# local dns_fallback_json="[]"
|
||||
# if [[ -n "$wg_dns_fallback" ]]; then
|
||||
# local fallback_array
|
||||
# fallback_array=$(echo "$wg_dns_fallback" | tr ',' '\n' | \
|
||||
# while IFS= read -r s; do
|
||||
# s="${s// /}"
|
||||
# [[ -n "$s" ]] && printf '"%s",' "$s"
|
||||
# done | sed 's/,$//')
|
||||
# dns_fallback_json="[${fallback_array}]"
|
||||
# fi
|
||||
# Build fallback DNS array
|
||||
local dns_fallback_json="[]"
|
||||
if [[ -n "$wg_dns_fallback" ]]; then
|
||||
local fallback_array
|
||||
fallback_array=$(echo "$wg_dns_fallback" | tr ',' '\n' | \
|
||||
while IFS= read -r s; do
|
||||
s="${s// /}"
|
||||
[[ -n "$s" ]] && printf '"%s",' "$s"
|
||||
done | sed 's/,$//')
|
||||
dns_fallback_json="[${fallback_array}]"
|
||||
fi
|
||||
|
||||
# mkdir -p "$(dirname "$output_file")"
|
||||
# cat > "$output_file" << JSON
|
||||
# {
|
||||
# "wireguard": {
|
||||
# "interface": "${wg_interface}",
|
||||
# "endpoint": "${wg_endpoint}",
|
||||
# "port": ${wg_port},
|
||||
# "subnet": "${wg_subnet}",
|
||||
# "lan": "${wg_lan}"
|
||||
# },
|
||||
# "dns": {
|
||||
# "primary": "${wg_dns}",
|
||||
# "fallback": ${dns_fallback_json}
|
||||
# },
|
||||
# "handshake": {
|
||||
# "check_interval_sec": ${wg_hs_check}
|
||||
# },
|
||||
# "activity": {
|
||||
# "total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||
# "current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||
# },
|
||||
# "display": {
|
||||
# "date_format": "${date_format}"
|
||||
# }
|
||||
# }
|
||||
# JSON
|
||||
# }
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
cat > "$output_file" << JSON
|
||||
{
|
||||
"wireguard": {
|
||||
"interface": "${wg_interface}",
|
||||
"endpoint": "${wg_endpoint}",
|
||||
"port": ${wg_port},
|
||||
"subnet": "${wg_subnet}",
|
||||
"lan": "${wg_lan}"
|
||||
},
|
||||
"dns": {
|
||||
"primary": "${wg_dns}",
|
||||
"fallback": ${dns_fallback_json}
|
||||
},
|
||||
"handshake": {
|
||||
"check_interval_sec": ${wg_hs_check}
|
||||
},
|
||||
"activity": {
|
||||
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||
},
|
||||
"display": {
|
||||
"date_format": "${date_format}"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
# function config::_write_default_json() {
|
||||
# local output_file="$1"
|
||||
# mkdir -p "$(dirname "$output_file")"
|
||||
# cat > "$output_file" << 'JSON'
|
||||
# {
|
||||
# "wireguard": {
|
||||
# "interface": "wg0",
|
||||
# "endpoint": "",
|
||||
# "port": 51820,
|
||||
# "subnet": "10.1.0.0/16",
|
||||
# "lan": "10.0.0.0/24"
|
||||
# },
|
||||
# "dns": {
|
||||
# "primary": "10.0.0.103",
|
||||
# "fallback": []
|
||||
# },
|
||||
# "handshake": {
|
||||
# "check_interval_sec": 300
|
||||
# },
|
||||
# "activity": {
|
||||
# "total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||
# "current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||
# },
|
||||
# "display": {
|
||||
# "date_format": "eu"
|
||||
# }
|
||||
# }
|
||||
# JSON
|
||||
# }
|
||||
function config::_write_default_json() {
|
||||
local output_file="$1"
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
cat > "$output_file" << 'JSON'
|
||||
{
|
||||
"wireguard": {
|
||||
"interface": "wg0",
|
||||
"endpoint": "",
|
||||
"port": 51820,
|
||||
"subnet": "10.1.0.0/16",
|
||||
"lan": "10.0.0.0/24"
|
||||
},
|
||||
"dns": {
|
||||
"primary": "10.0.0.103",
|
||||
"fallback": []
|
||||
},
|
||||
"handshake": {
|
||||
"check_interval_sec": 300
|
||||
},
|
||||
"activity": {
|
||||
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||
},
|
||||
"display": {
|
||||
"date_format": "eu"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/config/config.sh — router only
|
||||
|
||||
function cmd::config::on_load() {
|
||||
command::helpers "helpers.sh"
|
||||
command::define show "Show client config" [*]
|
||||
command::define migrate "Migrate config to JSON" [m]
|
||||
}
|
||||
|
||||
hook::on "command:help:config" command::help::auto
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/config/helpers.sh
|
||||
|
||||
function cmd::config::_migrate_impl() {
|
||||
local force="$1" dry_run="$2"
|
||||
|
||||
local wgctl_dir; wgctl_dir="$(ctx::wgctl)"
|
||||
local config_dir="${wgctl_dir}/config"
|
||||
local data_dir="${wgctl_dir}/data"
|
||||
local legacy_conf="${wgctl_dir}/wgctl.conf"
|
||||
local json_conf="${config_dir}/wgctl.json"
|
||||
|
||||
if [[ -f "$json_conf" && ! -f "$legacy_conf" ]]; then
|
||||
log::wg_warning "Already migrated to new config structure"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::section "wgctl Config Migration"
|
||||
printf "\n"
|
||||
printf " This will:\n"
|
||||
printf " 1. Create %s/config/ and %s/data/\n" "$wgctl_dir" "$wgctl_dir"
|
||||
printf " 2. Convert wgctl.conf → wgctl.json\n"
|
||||
printf " 3. Move data files to data/\n\n"
|
||||
|
||||
if [[ "$force" != "true" && "$dry_run" != "true" ]]; then
|
||||
read -r -p " Proceed? [y/N] " confirm
|
||||
case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac
|
||||
fi
|
||||
|
||||
$dry_run || mkdir -p "$config_dir" "$data_dir"
|
||||
$dry_run && printf " Would create: %s/config/\n" "$wgctl_dir"
|
||||
$dry_run && printf " Would create: %s/data/\n" "$wgctl_dir"
|
||||
|
||||
if [[ -f "$legacy_conf" ]]; then
|
||||
$dry_run || config::_convert_to_json "$legacy_conf" "$json_conf"
|
||||
printf " %s wgctl.conf → config/wgctl.json\n" \
|
||||
"$($dry_run && echo '[dry-run]' || echo '✓')"
|
||||
else
|
||||
log::wg_warning "No wgctl.conf found — creating default wgctl.json"
|
||||
$dry_run || config::_write_default_json "$json_conf"
|
||||
fi
|
||||
|
||||
local -a data_files=(hosts.json services.json subnets.json policies.json)
|
||||
local -a data_dirs=(rules identities groups blocks meta peer-history)
|
||||
|
||||
for f in "${data_files[@]}"; do
|
||||
if [[ -f "${wgctl_dir}/${f}" ]]; then
|
||||
$dry_run || mv "${wgctl_dir}/${f}" "${data_dir}/${f}"
|
||||
printf " %s %s → data/%s\n" \
|
||||
"$($dry_run && echo '[dry-run]' || echo '✓')" "$f" "$f"
|
||||
fi
|
||||
done
|
||||
|
||||
for d in "${data_dirs[@]}"; do
|
||||
if [[ -d "${wgctl_dir}/${d}" ]]; then
|
||||
$dry_run || mv "${wgctl_dir}/${d}" "${data_dir}/${d}"
|
||||
printf " %s %s/ → data/%s/\n" \
|
||||
"$($dry_run && echo '[dry-run]' || echo '✓')" "$d" "$d"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$dry_run" != "true" && -f "$legacy_conf" ]]; then
|
||||
mv "$legacy_conf" "${legacy_conf}.bak"
|
||||
printf " ✓ wgctl.conf → wgctl.conf.bak (backup)\n"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
[[ "$dry_run" == "true" ]] \
|
||||
&& log::wg_warning "Dry run — no changes made" \
|
||||
|| log::wg_success "Migration complete"
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function config::_convert_to_json() {
|
||||
local legacy_file="$1" output_file="$2"
|
||||
|
||||
# Read legacy conf into variables
|
||||
local wg_interface="wg0" wg_endpoint="" wg_dns="10.0.0.103"
|
||||
local wg_dns_fallback="" wg_port="51820" wg_subnet="10.1.0.0/16"
|
||||
local wg_lan="10.0.0.0/24" wg_hs_check="300" date_format="eu"
|
||||
|
||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ -z "${key// }" ]] && continue
|
||||
key="${key// /}"
|
||||
value="${value// /}"
|
||||
case "$key" in
|
||||
WG_INTERFACE) wg_interface="$value" ;;
|
||||
WG_ENDPOINT) wg_endpoint="$value" ;;
|
||||
WG_DNS) wg_dns="$value" ;;
|
||||
WG_DNS_FALLBACK) wg_dns_fallback="$value" ;;
|
||||
WG_PORT) wg_port="$value" ;;
|
||||
WG_SUBNET) wg_subnet="$value" ;;
|
||||
WG_LAN) wg_lan="$value" ;;
|
||||
WG_HANDSHAKE_CHECK_TIME_SEC) wg_hs_check="$value" ;;
|
||||
DATE_FORMAT) date_format="$value" ;;
|
||||
esac
|
||||
done < "$legacy_file"
|
||||
|
||||
# Build fallback DNS array
|
||||
local dns_fallback_json="[]"
|
||||
if [[ -n "$wg_dns_fallback" ]]; then
|
||||
local fallback_array
|
||||
fallback_array=$(echo "$wg_dns_fallback" | tr ',' '\n' | \
|
||||
while IFS= read -r s; do
|
||||
s="${s// /}"
|
||||
[[ -n "$s" ]] && printf '"%s",' "$s"
|
||||
done | sed 's/,$//')
|
||||
dns_fallback_json="[${fallback_array}]"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
cat > "$output_file" << JSON
|
||||
{
|
||||
"wireguard": {
|
||||
"interface": "${wg_interface}",
|
||||
"endpoint": "${wg_endpoint}",
|
||||
"port": ${wg_port},
|
||||
"subnet": "${wg_subnet}",
|
||||
"lan": "${wg_lan}"
|
||||
},
|
||||
"dns": {
|
||||
"primary": "${wg_dns}",
|
||||
"fallback": ${dns_fallback_json}
|
||||
},
|
||||
"handshake": {
|
||||
"check_interval_sec": ${wg_hs_check}
|
||||
},
|
||||
"activity": {
|
||||
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||
},
|
||||
"display": {
|
||||
"date_format": "${date_format}"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
||||
function config::_write_default_json() {
|
||||
local output_file="$1"
|
||||
mkdir -p "$(dirname "$output_file")"
|
||||
cat > "$output_file" << 'JSON'
|
||||
{
|
||||
"wireguard": {
|
||||
"interface": "wg0",
|
||||
"endpoint": "",
|
||||
"port": 51820,
|
||||
"subnet": "10.1.0.0/16",
|
||||
"lan": "10.0.0.0/24"
|
||||
},
|
||||
"dns": {
|
||||
"primary": "10.0.0.103",
|
||||
"fallback": []
|
||||
},
|
||||
"handshake": {
|
||||
"check_interval_sec": 300
|
||||
},
|
||||
"activity": {
|
||||
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||
},
|
||||
"display": {
|
||||
"date_format": "eu"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/config/migrate.sh
|
||||
|
||||
function cmd::config::migrate::on_load() {
|
||||
flag::define --force bool "Skip confirmation" section:Options
|
||||
flag::define --dry-run bool "Show what would be done" section:Options
|
||||
}
|
||||
|
||||
function cmd::config::migrate::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local force=false dry_run=false
|
||||
flag::bool --force && force=true
|
||||
flag::bool --dry-run && dry_run=true
|
||||
|
||||
cmd::config::_migrate_impl "$force" "$dry_run"
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/config/show.sh
|
||||
|
||||
function cmd::config::show::on_load() {
|
||||
help::section "Filters"
|
||||
flag::define --name value "Peer name" label:name required:true section:Filters
|
||||
flag::define --type value "Filter by type" label:type section:Filters
|
||||
}
|
||||
|
||||
function cmd::config::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local name; name=$(flag::value --name)
|
||||
local type; type=$(flag::value --type)
|
||||
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
|
||||
local conf
|
||||
conf="$(ctx::clients)/${name}.conf"
|
||||
|
||||
log::section "Client Config: ${name}"
|
||||
cat "$conf"
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/add.sh
|
||||
|
||||
function cmd::group::add::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --desc value "Group description" label:desc
|
||||
}
|
||||
|
||||
function cmd::group::add::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local desc; desc=$(flag::value --desc)
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
if group::exists "$name"; then
|
||||
log::error "Group already exists: ${name}"; return 1
|
||||
fi
|
||||
json::create_group "$(group::path "$name")" "$name" "$desc"
|
||||
log::wg_success "Group created: ${name}"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/audit.sh
|
||||
|
||||
function cmd::group::audit::on_load() {
|
||||
flag::define --name value "Group name (or all groups)" label:name
|
||||
}
|
||||
|
||||
function cmd::group::audit::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
cmd::group::_audit_impl "$name"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/block.sh
|
||||
|
||||
function cmd::group::block::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --reason value "Block reason" label:reason
|
||||
flag::define --quiet bool "Suppress output"
|
||||
}
|
||||
|
||||
function cmd::group::block::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local reason; reason=$(flag::value --reason)
|
||||
local quiet=false; flag::bool --quiet && quiet=true
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
cmd::group::_block_impl "$name" "$reason" "$quiet"
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/group.sh — router only
|
||||
|
||||
function cmd::group::on_load() {
|
||||
command::helpers "helpers.sh"
|
||||
|
||||
command::define list "List all groups" [*, ls]
|
||||
command::define show "Show group details" [info]
|
||||
command::define add "Create a new group" [new, create]
|
||||
command::define remove "Remove a group" [rm, del, delete]
|
||||
command::define rename "Rename a group"
|
||||
command::define peer "Manage group peers"
|
||||
command::define rm-peers "Remove all peers from a group" [rm-peers]
|
||||
command::define set-main "Set main group for a peer" [set-main]
|
||||
command::define block "Block all peers in a group"
|
||||
command::define unblock "Unblock all peers in a group"
|
||||
command::define rule "Manage group rules"
|
||||
command::define purge-stale "Remove stale peers from groups" [purge]
|
||||
command::define audit "Audit group membership"
|
||||
command::define logs "Show logs for group peers"
|
||||
command::define watch "Watch logs for group peers"
|
||||
}
|
||||
|
||||
hook::on "command:help:group" command::help::auto
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/helpers.sh
|
||||
# All group implementation logic — called by subcommand run functions
|
||||
|
||||
# ── Show ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_show_impl() {
|
||||
local name="$1"
|
||||
local group_file; group_file="$(group::path "$name")"
|
||||
|
||||
log::section "Group: ${name}"
|
||||
printf "\n"
|
||||
|
||||
local desc; desc=$(json::get "$group_file" "desc")
|
||||
ui::row "Description" "${desc:-—}"
|
||||
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(json::get "$group_file" "peers") || true
|
||||
local filtered=()
|
||||
for p in "${peers_list[@]:-}"; do
|
||||
[[ -n "$p" ]] && filtered+=("$p")
|
||||
done
|
||||
peers_list=("${filtered[@]:-}")
|
||||
local peer_count=${#peers_list[@]}
|
||||
[[ -z "${peers_list[0]:-}" ]] && peer_count=0
|
||||
|
||||
local valid_count=0
|
||||
for p in "${peers_list[@]}"; do
|
||||
[[ -z "$p" ]] && continue
|
||||
peers::require_exists "$p" >/dev/null 2>&1 && (( valid_count++ )) || true
|
||||
done
|
||||
local peer_word="peers"
|
||||
[[ "$valid_count" -eq 1 ]] && peer_word="peer"
|
||||
ui::row "Peers" "${valid_count} ${peer_word}"
|
||||
printf "\n"
|
||||
|
||||
if [[ "$peer_count" -gt 0 ]]; then
|
||||
local w_name=16 w_ip=13
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
(( ${#peer_name} > w_name )) && w_name=${#peer_name}
|
||||
done
|
||||
(( w_name += 2 ))
|
||||
ui::group::show_peers peers_list "$w_name" "$w_ip"
|
||||
else
|
||||
printf " \033[2m—\033[0m\n"
|
||||
fi
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ── Rename ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_rename_impl() {
|
||||
local name="$1" new_name="$2"
|
||||
local old_file; old_file="$(group::path "$name")"
|
||||
local new_file; new_file="$(group::path "$new_name")"
|
||||
json::set "$old_file" "name" "\"$new_name\""
|
||||
mv "$old_file" "$new_file"
|
||||
log::wg_success "Group renamed: ${name} → ${new_name}"
|
||||
}
|
||||
|
||||
# ── Peer add/remove ───────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::peer_add_impl() {
|
||||
local name="$1" peer="$2"
|
||||
group::require_exists "$name" || return 1
|
||||
peers::require_exists "$peer" || return 1
|
||||
if group::has_peer "$name" "$peer"; then
|
||||
log::wg_warning "Peer '${peer}' is already in group '${name}'"
|
||||
return 0
|
||||
fi
|
||||
group::add_peer "$name" "$peer"
|
||||
log::wg_success "Added '${peer}' to group '${name}'"
|
||||
}
|
||||
|
||||
function cmd::group::peer_remove_impl() {
|
||||
local name="$1" peer="$2"
|
||||
group::require_exists "$name" || return 1
|
||||
if ! group::has_peer "$name" "$peer"; then
|
||||
log::wg_warning "Peer '${peer}' is not in group '${name}'"
|
||||
return 0
|
||||
fi
|
||||
group::remove_peer "$name" "$peer"
|
||||
log::wg_success "Removed '${peer}' from group '${name}'"
|
||||
}
|
||||
|
||||
# ── Remove peers from WireGuard ───────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_rm_peers_impl() {
|
||||
local name="$1" force="$2"
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
local peer_count=${#peers_list[@]}
|
||||
[[ -z "${peers_list[0]:-}" ]] && peer_count=0
|
||||
|
||||
if [[ "$peer_count" -eq 0 ]]; then
|
||||
log::wg_warning "Group '${name}' has no peers"; return 0
|
||||
fi
|
||||
|
||||
if [[ "$force" != "true" ]]; then
|
||||
read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm
|
||||
case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac
|
||||
fi
|
||||
|
||||
load_command remove
|
||||
group::each_peer "$name" cmd::group::_rm_peer_cb
|
||||
log::wg_success "Removed peers from group '${name}' (definition kept)"
|
||||
}
|
||||
|
||||
function cmd::group::_rm_peer_cb() {
|
||||
local peer_name="${1:-}"
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"; return 0
|
||||
fi
|
||||
cmd::remove::run --name "$peer_name" --force
|
||||
}
|
||||
|
||||
# ── Set main ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_set_main_impl() {
|
||||
local name="$1" peer="$2"
|
||||
group::require_exists "$name" || return 1
|
||||
peer=$(peers::resolve_and_require "$peer" "") || return 1
|
||||
if ! group::has_peer "$name" "$peer"; then
|
||||
log::error "Peer '${peer}' is not in group '${name}'"
|
||||
log::info "Add them first: wgctl group peer --name ${name} --peer ${peer} --action add"
|
||||
return 1
|
||||
fi
|
||||
peers::set_main_group "$peer" "$name"
|
||||
log::wg_success "Main group for '${peer}' set to '${name}'"
|
||||
}
|
||||
|
||||
# ── Block ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_block_impl() {
|
||||
local name="$1"
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
local filtered=()
|
||||
for p in "${peers_list[@]:-}"; do [[ -n "$p" ]] && filtered+=("$p"); done
|
||||
[[ ${#filtered[@]} -eq 0 ]] && \
|
||||
log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
local count=0 skipped=0
|
||||
for peer_name in "${filtered[@]}"; do
|
||||
if cmd::group::_block_peer "$peer_name" "$name"; then
|
||||
(( count++ )) || true
|
||||
else
|
||||
(( skipped++ )) || true
|
||||
fi
|
||||
done
|
||||
|
||||
[[ "$count" -gt 0 ]] && log::wg_block "All peers from ${name} have been blocked (${count} peers)."
|
||||
[[ "$skipped" -gt 0 ]] && log::wg_warning "${skipped} peers already blocked"
|
||||
}
|
||||
|
||||
function cmd::group::_block_peer() {
|
||||
local peer_name="${1:-}" group_name="${2:-}"
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"; return 0
|
||||
fi
|
||||
|
||||
local client_ip; client_ip=$(peers::get_ip "$peer_name")
|
||||
local current_blocked_groups; current_blocked_groups=$(block::get_groups "$peer_name")
|
||||
|
||||
local IFS=','
|
||||
for g in $current_blocked_groups; do
|
||||
if [[ "$g" == "$group_name" ]]; then
|
||||
log::wg_warning "${peer_name} — already blocked by group '${group_name}'"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
unset IFS
|
||||
|
||||
block::add_group "$peer_name" "$client_ip" "$group_name"
|
||||
peers::exists_in_server "$peer_name" && block::apply_full "$peer_name" "$client_ip"
|
||||
}
|
||||
|
||||
# ── Unblock ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_unblock_impl() {
|
||||
local name="$1"
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
local filtered=()
|
||||
for p in "${peers_list[@]:-}"; do [[ -n "$p" ]] && filtered+=("$p"); done
|
||||
[[ ${#filtered[@]} -eq 0 ]] && \
|
||||
log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
local count=0 skipped=0
|
||||
for peer_name in "${filtered[@]}"; do
|
||||
if cmd::group::_unblock_peer "$peer_name" "$name"; then
|
||||
(( count++ )) || true
|
||||
else
|
||||
(( skipped++ )) || true
|
||||
fi
|
||||
done
|
||||
|
||||
[[ "$count" -gt 0 ]] && log::wg_unblock "All peers from ${name} have been unblocked."
|
||||
[[ "$skipped" -gt 0 ]] && \
|
||||
log::wg_warning "${skipped} peer(s) remain blocked (blocked directly or by other groups)"
|
||||
}
|
||||
|
||||
function cmd::group::_unblock_peer() {
|
||||
local peer_name="${1:-}" group_name="${2:-}"
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"; return 1
|
||||
fi
|
||||
|
||||
if ! block::has_file "$peer_name"; then
|
||||
log::wg_warning "${peer_name} — not blocked"; return 1
|
||||
fi
|
||||
|
||||
local current_groups; current_groups=$(block::get_groups "$peer_name")
|
||||
if [[ "$current_groups" != *"$group_name"* ]]; then
|
||||
log::wg_warning "${peer_name} — not blocked by group '${group_name}'"; return 1
|
||||
fi
|
||||
|
||||
local client_ip; client_ip=$(peers::get_ip "$peer_name")
|
||||
block::remove_group "$peer_name" "$client_ip" "$group_name"
|
||||
|
||||
if block::is_blocked "$peer_name"; then
|
||||
local groups; groups=$(block::get_groups "$peer_name")
|
||||
local direct; direct=$(block::is_blocked_direct "$peer_name")
|
||||
if [[ "$direct" == "true" ]]; then
|
||||
log::wg_warning "${peer_name} — still blocked directly, skipping"
|
||||
else
|
||||
log::wg_warning "${peer_name} — still blocked by group(s): ${groups}, skipping"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
block::restore_peer "$peer_name" "$client_ip"
|
||||
block::remove_file "$peer_name"
|
||||
local rule; rule=$(peers::get_meta "$peer_name" "rule")
|
||||
[[ -n "$rule" ]] && rule::exists "$rule" && rule::apply "$rule" "$client_ip" "$peer_name"
|
||||
}
|
||||
|
||||
# ── Rule assign ───────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_rule_assign_impl() {
|
||||
local name="$1" rule="$2"
|
||||
group::require_exists "$name" || return 1
|
||||
rule::require_exists "$rule" || return 1
|
||||
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
[[ -z "${peers_list[0]:-}" ]] && \
|
||||
log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
group::each_peer "$name" cmd::group::_rule_assign_cb "$rule"
|
||||
log::wg_success "Assigned rule '${rule}' to group '${name}'"
|
||||
}
|
||||
|
||||
function cmd::group::_rule_assign_cb() {
|
||||
local peer_name="${1:-}" rule="${2:-}"
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"; return 0
|
||||
fi
|
||||
load_command rule
|
||||
cmd::rule::assign --name "$rule" --peer "$peer_name"
|
||||
}
|
||||
|
||||
function cmd::group::_rule_unassign_impl() {
|
||||
local name="$1" rule="$2"
|
||||
log::error "rule unassign not yet implemented"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Purge stale ───────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_purge_stale_impl() {
|
||||
local name="$1" all="$2" force="$3" dry_run="$4"
|
||||
local -a groups=()
|
||||
if [[ "$all" == "true" ]]; then
|
||||
while IFS= read -r group_file; do
|
||||
groups+=("$(basename "$group_file" .group)")
|
||||
done < <(find "$(ctx::groups)" -name "*.group" 2>/dev/null | sort)
|
||||
else
|
||||
group::require_exists "$name" || return 1
|
||||
groups=("$name")
|
||||
fi
|
||||
|
||||
local total_removed=0
|
||||
for group_name in "${groups[@]}"; do
|
||||
[[ -z "$group_name" ]] && continue
|
||||
local -a stale=()
|
||||
while IFS= read -r peer_name; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
[[ ! -f "$(ctx::clients)/${peer_name}.conf" ]] && stale+=("$peer_name")
|
||||
done < <(group::peers "$group_name" 2>/dev/null)
|
||||
[[ ${#stale[@]} -eq 0 ]] && continue
|
||||
|
||||
if [[ "$force" != "true" && "$dry_run" != "true" ]]; then
|
||||
printf " Group '%s' has %d stale peer(s): %s\n" \
|
||||
"$group_name" "${#stale[@]}" "${stale[*]}"
|
||||
read -r -p " Remove them? [y/N] " confirm
|
||||
case "$confirm" in [yY]*) ;; *) log::info "Skipped '${group_name}'"; continue ;; esac
|
||||
fi
|
||||
|
||||
local group_file; group_file="$(group::path "$group_name")"
|
||||
for peer_name in "${stale[@]}"; do
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
printf " \033[2m[dry-run]\033[0m Would remove '%s' from '%s'\n" \
|
||||
"$peer_name" "$group_name"
|
||||
else
|
||||
json::remove "$group_file" "peers" "$peer_name" 2>/dev/null || true
|
||||
log::debug "Removed stale peer '${peer_name}' from group '${group_name}'"
|
||||
fi
|
||||
(( total_removed++ )) || true
|
||||
done
|
||||
done
|
||||
|
||||
local action="Removed"
|
||||
[[ "$dry_run" == "true" ]] && action="Would remove"
|
||||
[[ "$total_removed" -eq 0 ]] && log::wg_warning "No stale peers found" && return 0
|
||||
log::wg_success "${action} ${total_removed} stale peer(s)"
|
||||
}
|
||||
|
||||
# ── Audit ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_audit_impl() {
|
||||
local name="$1"
|
||||
local -a groups=()
|
||||
if [[ -n "$name" ]]; then
|
||||
group::require_exists "$name" || return 1
|
||||
groups=("$name")
|
||||
else
|
||||
while IFS= read -r group_file; do
|
||||
groups+=("$(basename "$group_file" .group)")
|
||||
done < <(find "$(ctx::groups)" -name "*.group" 2>/dev/null | sort)
|
||||
fi
|
||||
|
||||
log::section "Group Audit"; echo ""
|
||||
for grp in "${groups[@]}"; do
|
||||
local -a all_peers=() stale=()
|
||||
while IFS= read -r p; do
|
||||
[[ -z "$p" ]] && continue
|
||||
all_peers+=("$p")
|
||||
[[ ! -f "$(ctx::clients)/${p}.conf" ]] && stale+=("$p")
|
||||
done < <(group::peers "$grp" 2>/dev/null)
|
||||
printf " %-20s %d peers" "$grp" "${#all_peers[@]}"
|
||||
[[ ${#stale[@]} -gt 0 ]] && printf " \033[1;31m%d stale\033[0m" "${#stale[@]}"
|
||||
printf "\n"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Logs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_logs_impl() {
|
||||
local name="$1" limit="$2" since="$3" fw="$4" wg="$5"
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
[[ -z "${peers_list[0]:-}" ]] && \
|
||||
log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
load_command logs
|
||||
command::load_subcmd logs show # ensure show_fw_events etc are loaded
|
||||
|
||||
log::section "Logs: Group '${name}'"
|
||||
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
if ! peers::require_exists "$peer_name" >/dev/null 2>&1; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
continue
|
||||
fi
|
||||
local filter_ip; filter_ip=$(peers::get_ip "$peer_name")
|
||||
local fw_out="" wg_out=""
|
||||
[[ "$wg" != "true" ]] && fw_out=$(cmd::logs::show_fw_events \
|
||||
"$filter_ip" "$peer_name" "" "$limit" "$(ctx::net)" \
|
||||
"1" "$since" "" "" "desc" "false" 2>/dev/null)
|
||||
[[ "$fw" != "true" ]] && wg_out=$(cmd::logs::show_wg_events \
|
||||
"$filter_ip" "$peer_name" "" "$limit" \
|
||||
"1" "$since" "" "desc" "false" 2>/dev/null)
|
||||
[[ -z "$fw_out" && -z "$wg_out" ]] && continue
|
||||
printf "\n \033[1;37m── %s ──\033[0m\n" "$peer_name"
|
||||
[[ -n "$fw_out" ]] && printf "%s\n" "$fw_out"
|
||||
[[ -n "$wg_out" ]] && printf "%s\n" "$wg_out"
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ── Watch ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_watch_impl() {
|
||||
local name="$1" fw="$2" wg="$3"
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
[[ -z "${peers_list[0]:-}" ]] && \
|
||||
log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
local filter_ips=""
|
||||
for peer in "${peers_list[@]}"; do
|
||||
[[ -z "$peer" ]] && continue
|
||||
local ip; ip=$(peers::get_ip "$peer")
|
||||
filter_ips+="${ip},"
|
||||
done
|
||||
filter_ips="${filter_ips%,}"
|
||||
|
||||
load_command logs
|
||||
load_command watch
|
||||
cmd::logs::follow "$filter_ips" "$name" "" "$fw" "$wg"
|
||||
}
|
||||
|
||||
|
||||
# ── Render/JSON ───────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::group::_render_table() {
|
||||
local data="$1" w_name="$2" w_desc="$3"
|
||||
ui::group::list_header_table
|
||||
while IFS="|" read -r name desc total blocked; do
|
||||
[[ -z "$name" ]] && continue
|
||||
ui::group::list_row_table "$name" "$desc" "$total" "$blocked"
|
||||
done <<< "$data"
|
||||
}
|
||||
|
||||
function cmd::group::_output_json() {
|
||||
local data
|
||||
data=$(json::group_list_data "$(ctx::groups)" "$(ctx::blocks)" "$(ctx::clients)")
|
||||
local -a groups=()
|
||||
while IFS="|" read -r name desc total blocked; do
|
||||
[[ -z "$name" ]] && continue
|
||||
groups+=("{\"name\":\"${name}\",\"description\":\"${desc}\",\"peers\":${total},\"blocked\":${blocked}}")
|
||||
done <<< "$data"
|
||||
local array; array=$(IFS=','; echo "${groups[*]:-}")
|
||||
printf '[%s]' "$array" | json::envelope "groups" "${#groups[@]}"
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/list.sh
|
||||
|
||||
function cmd::group::list::on_load() {
|
||||
command::mixin json_output section:Output
|
||||
}
|
||||
|
||||
function cmd::group::list::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local groups_dir; groups_dir="$(ctx::groups)"
|
||||
local groups=("${groups_dir}"/*.group)
|
||||
if [[ ! -f "${groups[0]}" ]]; then
|
||||
log::wg "No groups configured"; return 0
|
||||
fi
|
||||
|
||||
if command::json; then
|
||||
cmd::group::_output_json; return 0
|
||||
fi
|
||||
|
||||
local data
|
||||
data=$(json::group_list_data "$groups_dir" "$(ctx::blocks)" "$(ctx::clients)")
|
||||
[[ -z "$data" ]] && log::wg "No groups configured" && return 0
|
||||
|
||||
local w_name=12 w_desc=16
|
||||
while IFS="|" read -r name desc total blocked; do
|
||||
[[ -z "$name" ]] && continue
|
||||
(( ${#name} > w_name )) && w_name=${#name}
|
||||
local desc_len=${#desc}; [[ -z "$desc" ]] && desc_len=1
|
||||
(( desc_len > w_desc )) && w_desc=$desc_len
|
||||
done <<< "$data"
|
||||
(( w_name += 2 )); (( w_desc += 2 ))
|
||||
|
||||
log::section "Groups"; echo ""
|
||||
|
||||
if display::is_table "group_list"; then
|
||||
cmd::group::_render_table "$data" "$w_name" "$w_desc"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS="|" read -r name desc total blocked; do
|
||||
[[ -z "$name" ]] && continue
|
||||
ui::group::list_row "$name" "$desc" "$total" "$blocked" "$w_name" "$w_desc"
|
||||
done <<< "$data"
|
||||
echo ""
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/logs.sh
|
||||
|
||||
function cmd::group::logs::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --limit value "Max results" default:50 type:int min:1
|
||||
flag::define --since value "Since duration" label:time
|
||||
flag::define --fw bool "Firewall only"
|
||||
flag::define --wg bool "WireGuard only"
|
||||
flag::exclusive --fw --wg
|
||||
}
|
||||
|
||||
function cmd::group::logs::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local limit; limit=$(flag::value --limit)
|
||||
local since; since=$(flag::value --since)
|
||||
local fw=false wg=false
|
||||
flag::bool --fw && fw=true
|
||||
flag::bool --wg && wg=true
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
cmd::group::_logs_impl "$name" "$limit" "$since" "$fw" "$wg"
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/peer.sh
|
||||
|
||||
function cmd::group::peer::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --peer value "Peer name" required label:peer
|
||||
flag::define --action value "Action" required choices:add,remove label:action
|
||||
}
|
||||
|
||||
function cmd::group::peer::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local peer; peer=$(flag::value --peer)
|
||||
local action; action=$(flag::value --action)
|
||||
case "$action" in
|
||||
add) cmd::group::peer_add_impl "$name" "$peer" ;;
|
||||
remove) cmd::group::peer_remove_impl "$name" "$peer" ;;
|
||||
esac
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/purge-stale.sh
|
||||
|
||||
function cmd::group::purge_stale::on_load() {
|
||||
flag::define --name value "Group name (or --all)" label:name
|
||||
flag::define --all bool "Purge all groups"
|
||||
flag::define --force bool "Skip confirmation"
|
||||
flag::define --dry-run bool "Show what would be removed"
|
||||
flag::exclusive --name --all
|
||||
}
|
||||
|
||||
function cmd::group::purge_stale::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local all=false force=false dry_run=false
|
||||
flag::bool --all && all=true
|
||||
flag::bool --force && force=true
|
||||
flag::bool --dry-run && dry_run=true
|
||||
[[ -z "$name" && "$all" == "false" ]] && \
|
||||
log::error "Specify --name or --all" && return 1
|
||||
cmd::group::_purge_stale_impl "$name" "$all" "$force" "$dry_run"
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/remove.sh
|
||||
|
||||
function cmd::group::remove::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --force bool "Skip confirmation"
|
||||
}
|
||||
|
||||
function cmd::group::remove::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local force=false; flag::bool --force && force=true
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
|
||||
if ! $force; then
|
||||
read -r -p "Remove group '${name}'? [y/N] " confirm
|
||||
case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac
|
||||
fi
|
||||
|
||||
rm -f "$(group::path "$name")"
|
||||
log::wg_success "Group removed: ${name}"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/rename.sh
|
||||
|
||||
function cmd::group::rename::on_load() {
|
||||
flag::define --name value "Current group name" required label:name
|
||||
flag::define --new-name value "New group name" required label:name
|
||||
}
|
||||
|
||||
function cmd::group::rename::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local new_name; new_name=$(flag::value --new-name)
|
||||
[[ -z "$name" || -z "$new_name" ]] && \
|
||||
log::error "Missing required flags: --name and --new-name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
if group::exists "$new_name"; then
|
||||
log::error "Group already exists: ${new_name}"; return 1
|
||||
fi
|
||||
cmd::group::_rename_impl "$name" "$new_name"
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/rm-peers.sh
|
||||
|
||||
function cmd::group::rm_peers::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --force bool "Skip confirmation"
|
||||
}
|
||||
|
||||
function cmd::group::rm_peers::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local force=false; flag::bool --force && force=true
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
cmd::group::_rm_peers_impl "$name" "$force"
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/rule.sh
|
||||
|
||||
function cmd::group::rule::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --rule value "Rule name" required label:rule
|
||||
flag::define --action value "Action" required choices:assign,unassign label:action
|
||||
}
|
||||
|
||||
function cmd::group::rule::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local rule; rule=$(flag::value --rule)
|
||||
local action; action=$(flag::value --action)
|
||||
case "$action" in
|
||||
assign) cmd::group::_rule_assign_impl "$name" "$rule" ;;
|
||||
unassign) cmd::group::_rule_unassign_impl "$name" "$rule" ;;
|
||||
esac
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/set-main.sh
|
||||
|
||||
function cmd::group::set_main::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --peer value "Peer name" required label:peer
|
||||
}
|
||||
|
||||
function cmd::group::set_main::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local peer; peer=$(flag::value --peer)
|
||||
[[ -z "$name" || -z "$peer" ]] && \
|
||||
log::error "Missing required flags: --name and --peer" && return 1
|
||||
cmd::group::_set_main_impl "$name" "$peer"
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/show.sh
|
||||
|
||||
function cmd::group::show::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
}
|
||||
|
||||
function cmd::group::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
cmd::group::_show_impl "$name"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/unblock.sh
|
||||
|
||||
function cmd::group::unblock::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --reason value "Unblock reason" label:reason
|
||||
flag::define --quiet bool "Suppress output"
|
||||
}
|
||||
|
||||
function cmd::group::unblock::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local reason; reason=$(flag::value --reason)
|
||||
local quiet=false; flag::bool --quiet && quiet=true
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
cmd::group::_unblock_impl "$name" "$reason" "$quiet"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/watch.sh
|
||||
|
||||
function cmd::group::watch::on_load() {
|
||||
flag::define --name value "Group name" required label:name
|
||||
flag::define --fw bool "Firewall only"
|
||||
flag::define --wg bool "WireGuard only"
|
||||
flag::exclusive --fw --wg
|
||||
}
|
||||
|
||||
function cmd::group::watch::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local fw=false wg=false
|
||||
flag::bool --fw && fw=true
|
||||
flag::bool --wg && wg=true
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
group::require_exists "$name" || return 1
|
||||
cmd::group::_watch_impl "$name" "$fw" "$wg"
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/list/list.sh — router only
|
||||
|
||||
function cmd::list::on_load() {
|
||||
command::define show "List WireGuard clients" [*]
|
||||
}
|
||||
|
||||
hook::on "command:help:list" command::help::auto
|
||||
|
|
@ -1,682 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/list/show.sh
|
||||
|
||||
function cmd::list::show::on_load() {
|
||||
command::mixin json_output [section="Output"]
|
||||
|
||||
help::section "Filters"
|
||||
flag::define --name value "Show single peer" label:name section:Filters
|
||||
flag::define --type value "Filter by device type" label:type section:Filters
|
||||
flag::define --rule value "Filter by rule" label:rule section:Filters
|
||||
flag::define --group value "Filter by group" label:group section:Filters
|
||||
flag::define --identity value "Filter by identity" label:identity section:Filters
|
||||
flag::define --online bool "Show online peers only" section:Filters
|
||||
flag::define --offline bool "Show offline peers only" section:Filters
|
||||
flag::define --restricted bool "Show restricted peers" section:Filters
|
||||
flag::define --blocked bool "Show blocked peers" section:Filters
|
||||
flag::define --allowed bool "Show allowed peers" section:Filters
|
||||
|
||||
help::section "Output"
|
||||
flag::define --detailed bool "Show detailed view" section:Output
|
||||
|
||||
flag::exclusive --online --offline --blocked --restricted --allowed
|
||||
}
|
||||
|
||||
function cmd::list::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local filter_type; filter_type=$(flag::value --type)
|
||||
local filter_rule; filter_rule=$(flag::value --rule)
|
||||
local filter_group; filter_group=$(flag::value --group)
|
||||
local filter_identity; filter_identity=$(flag::value --identity)
|
||||
local single_name; single_name=$(flag::value --name)
|
||||
local online_only=false offline_only=false
|
||||
local restricted_only=false blocked_only=false allowed_only=false
|
||||
local detailed=false
|
||||
|
||||
flag::bool --online && online_only=true
|
||||
flag::bool --offline && offline_only=true
|
||||
flag::bool --restricted && restricted_only=true
|
||||
flag::bool --blocked && blocked_only=true
|
||||
flag::bool --allowed && allowed_only=true
|
||||
flag::bool --detailed && detailed=true
|
||||
|
||||
if [[ -n "$single_name" ]]; then
|
||||
cmd::list::show_client "$single_name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local dir
|
||||
dir="$(ctx::clients)"
|
||||
local confs=("${dir}"/*.conf)
|
||||
if [[ ! -f "${confs[0]}" ]]; then
|
||||
log::wg_warning "No clients configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cmd::list::_precompute_all
|
||||
|
||||
declare -gA p_identity_filter=()
|
||||
if [[ -n "$filter_identity" ]]; then
|
||||
identity::require_exists "$filter_identity" || return 1
|
||||
while IFS= read -r peer_name; do
|
||||
[[ -n "$peer_name" ]] && p_identity_filter["$peer_name"]=1
|
||||
done < <(identity::peers "$filter_identity")
|
||||
if [[ ${#p_identity_filter[@]} -eq 0 ]]; then
|
||||
log::wg_warning "Identity '${filter_identity}' has no peers"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log::section "WireGuard Clients"
|
||||
|
||||
local collected_rows=""
|
||||
collected_rows=$(cmd::list::_collect_all_rows | ui::sort_rows)
|
||||
if [[ -z "$collected_rows" ]]; then
|
||||
log::wg_warning "No results found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command::json; then
|
||||
cmd::list::_output_json "$collected_rows"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if $detailed; then
|
||||
cmd::list::_render_detailed "$collected_rows"
|
||||
cmd::list::_render_summary_from_rows "$collected_rows"
|
||||
return 0
|
||||
fi
|
||||
|
||||
display::render "peer_list" "$collected_rows" \
|
||||
"cmd::list::_render_compact" "cmd::list::_render_table"
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# ============================================
|
||||
# Detail Card
|
||||
# ============================================
|
||||
|
||||
function cmd::list::show_client() {
|
||||
local name="${1:-}"
|
||||
local conf
|
||||
conf="$(ctx::clients)/${name}.conf"
|
||||
|
||||
if [[ ! -f "$conf" ]]; then
|
||||
log::error "Client not found: ${name}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local ip
|
||||
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||
|
||||
local allowed_ips
|
||||
allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs)
|
||||
|
||||
local public_key
|
||||
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
||||
|
||||
local type
|
||||
type=$(peers::get_meta "$name" "type" 2>/dev/null)
|
||||
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
||||
|
||||
local endpoint="—"
|
||||
local ep
|
||||
ep=$(monitor::endpoint_for_key "$public_key")
|
||||
[[ -z "$ep" ]] && ep=$(monitor::last_endpoint "$name")
|
||||
[[ -n "$ep" ]] && endpoint="$ep"
|
||||
|
||||
local is_blocked="false"
|
||||
peers::is_blocked "$name" && is_blocked="true"
|
||||
|
||||
local is_restricted="false"
|
||||
peers::is_restricted "$name" && is_restricted="true"
|
||||
|
||||
local handshake_ts
|
||||
handshake_ts=$(monitor::get_handshake_ts "$public_key")
|
||||
|
||||
local last_ts
|
||||
last_ts=$(monitor::last_attempt "$name")
|
||||
|
||||
local status
|
||||
status=$(peers::format_status_verbose "$name" "$public_key" \
|
||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||
|
||||
local last_seen
|
||||
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||
|
||||
local blocks=""
|
||||
if block::has_file "$name" && block::is_blocked "$name"; then
|
||||
if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then
|
||||
blocks="all traffic blocked"
|
||||
fi
|
||||
local rule_lines
|
||||
rule_lines=$(block::format_rules "$name")
|
||||
[[ -n "$rule_lines" ]] && blocks+="$rule_lines"
|
||||
fi
|
||||
|
||||
ui::section "Client: ${name}"
|
||||
ui::row "IP" "$ip"
|
||||
ui::row "Type" "$(peers::display_type "$type")"
|
||||
ui::row "Status" "$(echo -e "$status")"
|
||||
ui::row "Endpoint" "$endpoint"
|
||||
ui::row "Last seen" "$last_seen"
|
||||
ui::row "AllowedIPs" "$allowed_ips"
|
||||
ui::row "Public key" "$public_key"
|
||||
|
||||
if [[ -z "$blocks" ]]; then
|
||||
ui::row "Blocks" "none"
|
||||
elif [[ "$blocks" == *"all traffic blocked"* ]]; then
|
||||
ui::row "Blocks" "$(echo -e "\033[1;31mAll\033[0m")"
|
||||
else
|
||||
printf " %-20s\n" "Blocks:"
|
||||
echo -e "$blocks"
|
||||
fi
|
||||
printf "\n"
|
||||
}
|
||||
# ============================================
|
||||
# Row collection (single pass, all filters)
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_collect_all_rows() {
|
||||
local dir
|
||||
dir="$(ctx::clients)"
|
||||
local _verbose_status="${LIST_VERBOSE_STATUS:-true}"
|
||||
|
||||
for conf in "${dir}"/*.conf; do
|
||||
[[ -f "$conf" ]] || continue
|
||||
local client_name
|
||||
client_name=$(basename "$conf" .conf)
|
||||
[[ -z "$client_name" ]] && continue
|
||||
|
||||
if [[ ${#p_identity_filter[@]} -gt 0 && \
|
||||
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local ip="${p_ips[$client_name]:-}"
|
||||
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||
[[ -z "$ip" ]] && continue
|
||||
|
||||
local type="${p_types[$client_name]:-unknown}"
|
||||
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||
|
||||
local pubkey="${p_pubkeys[$client_name]:-}"
|
||||
local handshake_ts="${wg_handshakes[$pubkey]:-0}"
|
||||
local is_blocked="${p_blocked[$client_name]:-false}"
|
||||
local is_restricted="${p_restricted[$client_name]:-false}"
|
||||
local last_ts="${p_last_ts[$client_name]:-}"
|
||||
local rule="${p_rules[$client_name]:-}"
|
||||
local group="${p_main_groups[$client_name]:-}"
|
||||
|
||||
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || continue; fi
|
||||
if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || continue; fi
|
||||
if $restricted_only && [[ "$is_restricted" != "true" ]]; then continue; fi
|
||||
if $blocked_only && [[ "$is_blocked" != "true" ]]; then continue; fi
|
||||
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||
[[ "$is_restricted" == "true" ]]; }; then continue; fi
|
||||
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then continue; fi
|
||||
if [[ -n "$filter_group" ]]; then
|
||||
local all_groups="${peer_group_map[$client_name]:-}"
|
||||
[[ "$all_groups" != *"$filter_group"* ]] && continue
|
||||
fi
|
||||
|
||||
# Resolve status — verbose or simple
|
||||
local status
|
||||
if [[ "$_verbose_status" == "true" ]]; then
|
||||
status=$(peers::format_status_verbose "$client_name" "$pubkey" \
|
||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts" | \
|
||||
sed 's/\x1b\[[0-9;]*m//g')
|
||||
else
|
||||
local state
|
||||
state=$(peers::connection_state "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||
status="${state%%|*}"
|
||||
fi
|
||||
|
||||
# Resolve last seen
|
||||
local last_seen="-"
|
||||
if [[ "$is_blocked" == "true" && -n "$last_ts" && "$last_ts" != "0" ]]; then
|
||||
local attempt_ts
|
||||
attempt_ts=$(json::iso_to_ts "$last_ts")
|
||||
last_seen="$(fmt::datetime_short "$attempt_ts") (dropped)"
|
||||
elif [[ -n "$handshake_ts" && "$handshake_ts" != "0" ]]; then
|
||||
local ts_display
|
||||
ts_display=$(fmt::datetime_short "$handshake_ts")
|
||||
if [[ "$status" == "online"* ]]; then
|
||||
last_seen="${ts_display} (handshake)"
|
||||
else
|
||||
last_seen="$ts_display"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%s|%s|%s|%s|%s|%s|%s|%s|%s\n" \
|
||||
"$client_name" "$ip" "$type" \
|
||||
"${rule:--}" "${group:--}" \
|
||||
"$status" "$last_seen" \
|
||||
"$is_blocked" "$is_restricted"
|
||||
done
|
||||
}
|
||||
# ============================================
|
||||
# Compact render
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_render_compact() {
|
||||
local rows="${1:-}"
|
||||
|
||||
# Measure column widths from pure values (fields 1-5, no labels)
|
||||
local w_name w_ip w_type w_rule w_group
|
||||
w_name=$(ui::measure_col "$rows" 1 14)
|
||||
w_ip=$(ui::measure_col "$rows" 2 13)
|
||||
w_type=$(ui::measure_col "$rows" 3 7)
|
||||
w_rule=$(ui::measure_col "$rows" 4 4)
|
||||
w_group=$(ui::measure_col "$rows" 5 4)
|
||||
|
||||
echo ""
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
ui::peer::list_row_compact \
|
||||
"$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" \
|
||||
"$name" "$ip" "$type" "$rule" "$group" \
|
||||
"$status" "$last_seen" "$is_blocked" "$is_restricted"
|
||||
done <<< "$rows"
|
||||
echo ""
|
||||
|
||||
cmd::list::_render_summary_from_rows "$rows"
|
||||
}
|
||||
# ============================================
|
||||
# Table render (kept for config switching)
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_render_table() {
|
||||
local rows="${1:-}"
|
||||
[[ -z "$rows" ]] && log::wg_warning "No results found" && return 0
|
||||
|
||||
# Measure column widths from data (same as compact)
|
||||
local w_name=16 w_ip=13 w_type=8 w_rule=10 w_group=10 w_status=10 w_last=20
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
(( ${#name} > w_name )) && w_name=${#name}
|
||||
(( ${#ip} > w_ip )) && w_ip=${#ip}
|
||||
(( ${#type} > w_type )) && w_type=${#type}
|
||||
(( ${#rule} > w_rule )) && w_rule=${#rule}
|
||||
(( ${#group} > w_group )) && w_group=${#group}
|
||||
(( ${#last_seen} > w_last )) && w_last=${#last_seen}
|
||||
local cs
|
||||
cs=$(printf "%s" "$status" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
(( ${#cs} > w_status )) && w_status=${#cs}
|
||||
done <<< "$rows"
|
||||
(( w_name += 2 )); (( w_ip += 2 ))
|
||||
(( w_type += 2 )); (( w_rule += 2 ))
|
||||
(( w_group += 2 )); (( w_last += 2 ))
|
||||
|
||||
# Header
|
||||
printf "\n %-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %-${w_status}s %s\n" \
|
||||
"NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
|
||||
printf " %s\n" "$(printf '─%.0s' {1..115})"
|
||||
|
||||
# Rows
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
local clean_status
|
||||
clean_status=$(echo "$status" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
local status_pad_n=$(( w_status - ${#clean_status} ))
|
||||
[[ $status_pad_n -lt 0 ]] && status_pad_n=0
|
||||
|
||||
local row_color status_color
|
||||
row_color=$(ui::peer::_row_color "$is_blocked" "$is_restricted" "$clean_status")
|
||||
status_color=$(ui::peer::status_color "$is_blocked" "$is_restricted" "$clean_status")
|
||||
|
||||
local status_colored="${status_color}${clean_status}\033[0m"
|
||||
|
||||
local last_seen_colored="$last_seen"
|
||||
[[ -n "$row_color" ]] && last_seen_colored="${row_color}${last_seen}\033[0m" \
|
||||
|| last_seen_colored="${status_color}${last_seen}\033[0m"
|
||||
|
||||
if [[ -n "$row_color" ]]; then
|
||||
printf " %b%-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %-${w_status}s %s\033[0m\n" \
|
||||
"$row_color" "$name" "$ip" "$type" "$rule" "$group" "$clean_status" "$last_seen"
|
||||
else
|
||||
printf " %-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %b%*s\033[0m %b\n" \
|
||||
"$name" "$ip" "$type" "$rule" "$group" \
|
||||
"$status_color${clean_status}" "$status_pad_n" "" \
|
||||
"$last_seen_colored"
|
||||
fi
|
||||
done <<< "$rows"
|
||||
|
||||
printf " %s\n" "$(printf '─%.0s' {1..115})"
|
||||
cmd::list::_render_summary_from_rows "$rows"
|
||||
}
|
||||
|
||||
function cmd::list::_iter_confs_table() {
|
||||
local dir
|
||||
dir="$(ctx::clients)"
|
||||
for conf in "${dir}"/*.conf; do
|
||||
[[ -f "$conf" ]] || continue
|
||||
local client_name
|
||||
client_name=$(basename "$conf" .conf)
|
||||
[[ -z "$client_name" ]] && continue
|
||||
|
||||
if [[ ${#p_identity_filter[@]} -gt 0 && \
|
||||
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local ip="${p_ips[$client_name]:-}"
|
||||
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||
|
||||
local type="${p_types[$client_name]:-unknown}"
|
||||
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||
|
||||
cmd::list::_render_row "$client_name" "$ip" "$type"
|
||||
done
|
||||
}
|
||||
# ============================================
|
||||
# Detailed render (grouped by identity)
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_render_detailed() {
|
||||
local rows="${1:-}"
|
||||
|
||||
# Measure widths
|
||||
local w_name w_ip w_type w_rule w_group w_subnet
|
||||
w_name=$(ui::measure_col "$rows" 1 14)
|
||||
w_ip=$(ui::measure_col "$rows" 2 13)
|
||||
w_type=$(ui::measure_col "$rows" 3 7)
|
||||
w_rule=$(ui::measure_col "$rows" 4 4)
|
||||
w_group=$(ui::measure_col "$rows" 5 4)
|
||||
# subnet not in rows — use fixed width
|
||||
w_subnet=10
|
||||
|
||||
# Group by identity
|
||||
declare -A identity_rows=()
|
||||
local no_identity_rows=""
|
||||
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
local id_name
|
||||
id_name=$(identity::get_name "$name")
|
||||
local row="${name}|${ip}|${type}|${rule}|${group}|${status}|${last_seen}|${is_blocked}|${is_restricted}"
|
||||
if [[ -n "$id_name" ]]; then
|
||||
identity_rows["$id_name"]+="${row}"$'\n'
|
||||
else
|
||||
no_identity_rows+="${row}"$'\n'
|
||||
fi
|
||||
done <<< "$rows"
|
||||
|
||||
echo ""
|
||||
|
||||
# Render identity groups (sorted)
|
||||
for id_name in $(echo "${!identity_rows[@]}" | tr ' ' '\n' | sort); do
|
||||
ui::peer::list_identity_header "$id_name"
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
local peer_type="${p_types[$name]:-}"
|
||||
subnet=$(peers::get_display_subnet "$name" "$peer_type")
|
||||
|
||||
ui::peer::list_row_detailed \
|
||||
"$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \
|
||||
"$name" "$ip" "$type" "$rule" "$group" "$subnet" \
|
||||
"$status" "$last_seen" "$is_blocked" "$is_restricted"
|
||||
done < <(echo "${identity_rows[$id_name]}" | ui::sort_rows)
|
||||
done
|
||||
|
||||
# Render peers without identity (no "other" header if empty)
|
||||
if [[ -n "$no_identity_rows" ]]; then
|
||||
local trimmed
|
||||
trimmed=$(echo "$no_identity_rows" | grep -v '^$')
|
||||
if [[ -n "$trimmed" ]]; then
|
||||
ui::peer::list_identity_header "other"
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
local subnet
|
||||
subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null)
|
||||
if [[ -z "$subnet" ]]; then
|
||||
local peer_type="${p_types[$name]:-}"
|
||||
[[ -n "$peer_type" ]] && subnet="$peer_type"
|
||||
fi
|
||||
[[ -z "$subnet" ]] && subnet="-"
|
||||
ui::peer::list_row_detailed \
|
||||
"$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \
|
||||
"$name" "$ip" "$type" "$rule" "$group" "$subnet" \
|
||||
"$status" "$last_seen" "$is_blocked" "$is_restricted"
|
||||
done <<< "$trimmed"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
# ============================================
|
||||
# Summary
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_render_summary_from_rows() {
|
||||
local rows="${1:-}"
|
||||
declare -A rule_counts=() group_counts=()
|
||||
local total=0
|
||||
|
||||
while IFS='|' read -r name ip type rule group rest; do
|
||||
[[ -z "$name" ]] && continue
|
||||
(( total++ )) || true
|
||||
rule_counts["${rule:--}"]=$(( ${rule_counts[${rule:--}]:-0} + 1 )) || true
|
||||
[[ "$group" != "-" && -n "$group" ]] && \
|
||||
group_counts["$group"]=$(( ${group_counts[$group]:-0} + 1 )) || true
|
||||
done <<< "$rows"
|
||||
|
||||
local rule_summary=""
|
||||
for r in $(echo "${!rule_counts[@]}" | tr ' ' '\n' | sort); do
|
||||
rule_summary+="${rule_counts[$r]} ${r}, "
|
||||
done
|
||||
rule_summary="${rule_summary%, }"
|
||||
|
||||
local group_summary=""
|
||||
for g in $(echo "${!group_counts[@]}" | tr ' ' '\n' | sort); do
|
||||
group_summary+="${group_counts[$g]} in ${g}, "
|
||||
done
|
||||
group_summary="${group_summary%, }"
|
||||
|
||||
if [[ -n "$group_summary" ]]; then
|
||||
printf " Showing %s peers [%s] — %s\n\n" "$total" "$rule_summary" "$group_summary"
|
||||
else
|
||||
printf " Showing %s peers [%s]\n\n" "$total" "$rule_summary"
|
||||
fi
|
||||
}
|
||||
# ============================================
|
||||
# Table row rendering
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_render_row() {
|
||||
local client_name="$1" ip="$2" type="$3"
|
||||
|
||||
local pubkey="${p_pubkeys[$client_name]:-}"
|
||||
local handshake_ts="${wg_handshakes[$pubkey]:-0}"
|
||||
local is_blocked="${p_blocked[$client_name]:-false}"
|
||||
local is_restricted="${p_restricted[$client_name]:-false}"
|
||||
local last_ts="${p_last_ts[$client_name]:-}"
|
||||
|
||||
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_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 $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
||||
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||
[[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
||||
|
||||
if [[ -n "$filter_group" ]]; then
|
||||
local all_groups="${peer_group_map[$client_name]:-}"
|
||||
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
||||
fi
|
||||
|
||||
local status last_seen display_type rule
|
||||
status=$(peers::format_status_verbose "$client_name" "$pubkey" \
|
||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||
display_type=$(peers::display_type "$type")
|
||||
rule="${p_rules[$client_name]:-—}"
|
||||
|
||||
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
||||
|
||||
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
||||
cmd::list::_render_header $has_groups
|
||||
_list_header_printed=true
|
||||
fi
|
||||
|
||||
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
||||
|
||||
local padded_status
|
||||
padded_status=$(ui::pad_status "$status" 25)
|
||||
|
||||
if $has_groups; then
|
||||
local main_group="${p_main_groups[$client_name]:-}"
|
||||
local group_display="${main_group:-${peer_group_map[$client_name]:-—}}"
|
||||
printf " %-28s %-15s %-13s %-12s %-12s %s %s\n" \
|
||||
"$client_name" "$ip" "$display_type" "$rule" \
|
||||
"$group_display" "$padded_status" "$last_seen"
|
||||
else
|
||||
printf " %-28s %-15s %-13s %-12s %s %s\n" \
|
||||
"$client_name" "$ip" "$display_type" "$rule" \
|
||||
"$padded_status" "$last_seen"
|
||||
fi
|
||||
}
|
||||
# ============================================
|
||||
# Precompute
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_precompute_all() {
|
||||
declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=()
|
||||
while IFS="|" read -r name ip rule type last_ts last_evt main_group; do
|
||||
[[ -z "$name" ]] && continue
|
||||
p_ips["$name"]="$ip"
|
||||
p_rules["$name"]="${rule:-}"
|
||||
p_types["$name"]="${type:-}"
|
||||
p_last_ts["$name"]="$last_ts"
|
||||
p_last_evt["$name"]="$last_evt"
|
||||
p_main_groups["$name"]="${main_group:-}"
|
||||
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
||||
|
||||
for name in "${!p_ips[@]}"; do
|
||||
[[ -n "${p_types[$name]:-}" ]] && continue
|
||||
p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
|
||||
done
|
||||
|
||||
declare -gA wg_handshakes=() wg_endpoints=()
|
||||
while IFS=$'\t' read -r pubkey ts; do
|
||||
[[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
||||
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
||||
while IFS=$'\t' read -r pubkey endpoint; do
|
||||
[[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
||||
done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
||||
|
||||
declare -gA p_blocked=() p_restricted=()
|
||||
cmd::list::_precompute_block_status p_blocked p_restricted
|
||||
|
||||
declare -gA p_pubkeys=()
|
||||
local dir
|
||||
dir="$(ctx::clients)"
|
||||
for kf in "${dir}"/*_public.key; do
|
||||
[[ -f "$kf" ]] || continue
|
||||
local kname
|
||||
kname=$(basename "$kf" _public.key)
|
||||
p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
||||
done
|
||||
|
||||
has_groups=false
|
||||
declare -gA peer_group_map=()
|
||||
local groups_dir
|
||||
groups_dir="$(ctx::groups)"
|
||||
local group_files=("${groups_dir}"/*.group)
|
||||
if [[ -f "${group_files[0]}" ]]; then
|
||||
has_groups=true
|
||||
while IFS=":" read -r peer_name group_name; do
|
||||
[[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
|
||||
done < <(json::peer_group_map "$groups_dir")
|
||||
fi
|
||||
|
||||
# Identity precompute (for --identity filter)
|
||||
declare -gA p_identity_filter=()
|
||||
}
|
||||
|
||||
function cmd::list::_precompute_block_status() {
|
||||
local -n _blocked="$1"
|
||||
local -n _restricted="$2"
|
||||
|
||||
local wg_peers
|
||||
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
||||
|
||||
while IFS= read -r name; do
|
||||
if block::has_specific_rules "$name" 2>/dev/null; then
|
||||
_restricted["$name"]=true
|
||||
else
|
||||
_restricted["$name"]=false
|
||||
fi
|
||||
|
||||
local pubkey
|
||||
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
||||
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
||||
_blocked["$name"]=true
|
||||
else
|
||||
_blocked["$name"]=false
|
||||
fi
|
||||
done < <(peers::all)
|
||||
}
|
||||
# ============================================
|
||||
# Header / Footer (table layout)
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_render_header() {
|
||||
ui::peer::list_header_table "$1"
|
||||
}
|
||||
|
||||
function cmd::list::_render_footer() {
|
||||
ui::peer::list_footer_table "$1"
|
||||
}
|
||||
|
||||
function cmd::list::_build_group_summary() {
|
||||
group_summary=""
|
||||
if $has_groups; then
|
||||
declare -A _gs=()
|
||||
for peer in "${!peer_group_map[@]}"; do
|
||||
local g="${peer_group_map[$peer]}"
|
||||
_gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true
|
||||
done
|
||||
for g in "${!_gs[@]}"; do
|
||||
group_summary+="${_gs[$g]} in ${g}, "
|
||||
done
|
||||
group_summary="${group_summary%, }"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::list::_show_client_safe() {
|
||||
local name="$1"
|
||||
cmd::list::show_client "$name" || true
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# JSON (API consumption)
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_output_json() {
|
||||
local rows="${1:-}"
|
||||
local -a peers=()
|
||||
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
|
||||
# Escape strings for JSON
|
||||
local peer_json
|
||||
peer_json=$(printf '{"name":"%s","ip":"%s","type":"%s","rule":"%s","group":"%s","status":"%s","last_seen":"%s","is_blocked":%s,"is_restricted":%s}' \
|
||||
"$name" "$ip" "$type" \
|
||||
"${rule}" "${group}" \
|
||||
"$status" "$last_seen" \
|
||||
"$is_blocked" "$is_restricted")
|
||||
peers+=("$peer_json")
|
||||
done <<< "$rows"
|
||||
|
||||
local count=${#peers[@]}
|
||||
local array
|
||||
# Join array with commas
|
||||
array=$(printf '%s\n' "${peers[@]}" | paste -sd ',' -)
|
||||
printf '{"peers":[%s]}' "$array" | json::envelope "list" "$count"
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/clean.sh
|
||||
|
||||
function cmd::logs::clean::on_load() {
|
||||
flag::define --force bool "Skip confirmation"
|
||||
flag::define --dry-run bool "Show what would be removed"
|
||||
}
|
||||
|
||||
function cmd::logs::clean::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local force=false dry_run=false
|
||||
flag::bool --force && force=true
|
||||
flag::bool --dry-run && dry_run=true
|
||||
|
||||
local dry_run_flag="0"
|
||||
$dry_run && dry_run_flag="1"
|
||||
|
||||
local count
|
||||
count=$(json::clean_handshakes \
|
||||
"$(ctx::events_log)" "$dry_run_flag" 2>/dev/null) || {
|
||||
log::error "Failed to clean handshakes"
|
||||
return 1
|
||||
}
|
||||
|
||||
if $dry_run; then
|
||||
log::wg_warning "Dry run — would remove ${count} keepalive handshake(s)"
|
||||
else
|
||||
log::wg_success "Removed ${count} keepalive handshake(s)"
|
||||
fi
|
||||
}
|
||||
|
|
@ -1,371 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/group/helpers.sh
|
||||
# All logs implementation logic — called by subcommand run functions
|
||||
|
||||
function cmd::logs::show_fw_events() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" net_file="${5:-}" collapse="${6:-1}" \
|
||||
since="${7:-}" filter_dest_ip="${8:-}" filter_dest_port="${9:-}" \
|
||||
sort_order="${10:-desc}" resolved_only="${11:-false}"
|
||||
|
||||
[[ ! -f "$FW_EVENTS_LOG" ]] && return 0
|
||||
|
||||
local data
|
||||
data=$(json::fw_events \
|
||||
"$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
||||
"$(ctx::clients)" "${net_file:-}" \
|
||||
"$limit" "$collapse" "$since" \
|
||||
"$filter_dest_ip" "$filter_dest_port" \
|
||||
"$sort_order" \
|
||||
2>/dev/null)
|
||||
|
||||
[[ -z "$data" ]] && return 0
|
||||
|
||||
# ── Collect unique endpoints for batch resolution ──
|
||||
local -a ep_list=()
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint; do
|
||||
[[ -z "$ts" || -z "$src_endpoint" ]] && continue
|
||||
ep_list+=("$src_endpoint")
|
||||
done <<< "$data"
|
||||
|
||||
declare -A resolve_cache=()
|
||||
if [[ ${#ep_list[@]} -gt 0 ]]; then
|
||||
while IFS='|' read -r ip name; do
|
||||
[[ -n "$ip" ]] && resolve_cache["$ip"]="$name"
|
||||
done < <(json::batch_resolve "${ep_list[@]}" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── Pass 1: measure widths ──
|
||||
local w_client=16 w_dest=20 w_endpoint=0
|
||||
local resolved_data=""
|
||||
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
|
||||
(( ${#client} > w_client )) && w_client=${#client}
|
||||
|
||||
local svc_display=""
|
||||
if [[ -n "$svc" ]]; then
|
||||
[[ -n "$dest_port" ]] && svc_display="${svc}/${proto}" \
|
||||
|| svc_display="${svc} (${proto})"
|
||||
else
|
||||
[[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \
|
||||
|| svc_display="${dest_ip} (${proto})"
|
||||
fi
|
||||
|
||||
local measure_len
|
||||
if $resolved_only; then
|
||||
measure_len=${#svc_display}
|
||||
else
|
||||
local raw_plain=""
|
||||
[[ -n "$svc" && -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})"
|
||||
[[ -n "$svc" && -z "$dest_port" ]] && raw_plain=" (${dest_ip})"
|
||||
measure_len=$(( ${#svc_display} + ${#raw_plain} ))
|
||||
fi
|
||||
(( measure_len > w_dest )) && w_dest=$measure_len
|
||||
|
||||
local src_resolved=""
|
||||
if [[ -n "$src_endpoint" ]]; then
|
||||
src_resolved="${resolve_cache[$src_endpoint]:-}"
|
||||
[[ "$src_resolved" == "$src_endpoint" ]] && src_resolved=""
|
||||
|
||||
local ep_measure_len
|
||||
if $resolved_only; then
|
||||
ep_measure_len=${#src_resolved}
|
||||
[[ -z "$src_resolved" ]] && ep_measure_len=${#src_endpoint}
|
||||
else
|
||||
ep_measure_len=${#src_endpoint}
|
||||
[[ -n "$src_resolved" ]] && \
|
||||
ep_measure_len=$(( ${#src_endpoint} + 4 + ${#src_resolved} ))
|
||||
fi
|
||||
(( ep_measure_len > w_endpoint )) && w_endpoint=$ep_measure_len
|
||||
fi
|
||||
|
||||
resolved_data+="${ts}|${client}|${dest_ip}|${dest_port}|${proto}|${svc}|${count}|${src_endpoint}|${src_resolved}"$'\n'
|
||||
done <<< "$data"
|
||||
|
||||
(( w_client += 2 ))
|
||||
(( w_dest += 2 ))
|
||||
[[ "$w_endpoint" -gt 0 ]] && (( w_endpoint += 2 ))
|
||||
|
||||
# ── Pass 2: render ──
|
||||
ui::logs::fw_section_header
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint src_resolved; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
ui::logs::fw_row "$ts" "$client" "$dest_ip" "$dest_port" \
|
||||
"$proto" "$svc" "$count" "$w_client" "$w_dest" \
|
||||
"$src_endpoint" "$src_resolved" "$w_endpoint" "$resolved_only"
|
||||
done <<< "$resolved_data"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function cmd::logs::show_wg_events() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" collapse="${5:-1}" \
|
||||
since="${6:-}" filter_event="${7:-}" sort_order="${8:-desc}" \
|
||||
resolved_only="${9:-false}"
|
||||
|
||||
[[ ! -f "$WG_EVENTS_LOG" ]] && return 0
|
||||
|
||||
local data
|
||||
data=$(json::wg_events \
|
||||
"$WG_EVENTS_LOG" "$filter_name" "$filter_type" \
|
||||
"$limit" "$collapse" "$since" "$filter_event" \
|
||||
"$(ctx::endpoint_cache)" "$sort_order" \
|
||||
2>/dev/null)
|
||||
|
||||
[[ -z "$data" ]] && return 0
|
||||
|
||||
# ── Collect unique endpoints for batch resolution ──
|
||||
local -a ep_list=()
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds; do
|
||||
[[ -z "$ts" || -z "$endpoint" ]] && continue
|
||||
ep_list+=("$endpoint")
|
||||
done <<< "$data"
|
||||
|
||||
declare -A resolve_cache=()
|
||||
if [[ ${#ep_list[@]} -gt 0 ]]; then
|
||||
while IFS='|' read -r ip name; do
|
||||
[[ -n "$ip" ]] && resolve_cache["$ip"]="$name"
|
||||
done < <(json::batch_resolve "${ep_list[@]}" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── Measure widths ──
|
||||
local w_client=16 w_endpoint=16
|
||||
local resolved_data=""
|
||||
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
(( ${#client} > w_client )) && w_client=${#client}
|
||||
|
||||
local resolved=""
|
||||
if [[ -n "$endpoint" ]]; then
|
||||
resolved="${resolve_cache[$endpoint]:-}"
|
||||
[[ "$resolved" == "$endpoint" ]] && resolved=""
|
||||
fi
|
||||
|
||||
local ep_raw="${endpoint:--}"
|
||||
local ep_measure_len
|
||||
if $resolved_only; then
|
||||
local ep_display="${resolved:-$endpoint}"
|
||||
[[ -z "$ep_display" ]] && ep_display="-"
|
||||
ep_measure_len=${#ep_display}
|
||||
else
|
||||
ep_measure_len=${#ep_raw}
|
||||
[[ -n "$resolved" && -n "$endpoint" ]] && \
|
||||
ep_measure_len=$(( ${#endpoint} + 4 + ${#resolved} ))
|
||||
fi
|
||||
(( ep_measure_len > w_endpoint )) && w_endpoint=$ep_measure_len
|
||||
|
||||
resolved_data+="${ts}|${client}|${endpoint}|${event}|${count}|${gap_seconds}|${resolved}"$'\n'
|
||||
done <<< "$data"
|
||||
|
||||
(( w_client += 2 ))
|
||||
(( w_endpoint += 2 ))
|
||||
|
||||
# ── Render ──
|
||||
ui::logs::wg_section_header
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds resolved; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
if $resolved_only; then
|
||||
local ep_display="${resolved:-$endpoint}"
|
||||
[[ -z "$ep_display" ]] && ep_display="-"
|
||||
ui::logs::wg_row "$ts" "$client" "$ep_display" "$event" \
|
||||
"$count" "$w_client" "$w_endpoint" "$gap_seconds" ""
|
||||
else
|
||||
ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \
|
||||
"$count" "$w_client" "$w_endpoint" "$gap_seconds" "$resolved"
|
||||
fi
|
||||
done <<< "$resolved_data"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function cmd::logs::show_merged() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" net_file="${5:-}" since="${6:-}"
|
||||
|
||||
local fw_data wg_data
|
||||
fw_data=$(json::fw_events \
|
||||
"$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
||||
"$(ctx::clients)" "${net_file:-}" \
|
||||
"$limit" "1" "$since" "" "" \
|
||||
2>/dev/null)
|
||||
wg_data=$(json::wg_events \
|
||||
"$WG_EVENTS_LOG" "$filter_name" "$filter_type" \
|
||||
"$limit" "1" "$since" "" \
|
||||
2>/dev/null)
|
||||
|
||||
local w_client=16 w_dest=20
|
||||
while IFS='|' read -r ts client rest; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
(( ${#client} > w_client )) && w_client=${#client}
|
||||
done < <(echo "$fw_data"; echo "$wg_data")
|
||||
(( w_client += 2 ))
|
||||
|
||||
local merged_data
|
||||
merged_data=$(
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
echo "fw|${ts}|${client}|${dest_ip}|${dest_port}|${proto}|${svc}|${count}"
|
||||
done <<< "$fw_data"
|
||||
while IFS='|' read -r ts client endpoint event count; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
echo "wg|${ts}|${client}|${endpoint}|${event}|${count}"
|
||||
done <<< "$wg_data"
|
||||
)
|
||||
|
||||
while IFS='|' read -r source ts rest; do
|
||||
[[ -z "$source" ]] && continue
|
||||
case "$source" in
|
||||
fw)
|
||||
IFS='|' read -r client dest_ip dest_port proto svc count <<< "$rest"
|
||||
local dest_display
|
||||
if [[ -n "$svc" ]]; then
|
||||
[[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})"
|
||||
else
|
||||
[[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})"
|
||||
fi
|
||||
(( ${#dest_display} > w_dest )) && w_dest=${#dest_display}
|
||||
;;
|
||||
esac
|
||||
done <<< "$merged_data"
|
||||
(( w_dest += 2 ))
|
||||
|
||||
while IFS='|' read -r source ts rest; do
|
||||
[[ -z "$source" ]] && continue
|
||||
case "$source" in
|
||||
fw)
|
||||
IFS='|' read -r client dest_ip dest_port proto svc count <<< "$rest"
|
||||
ui::watch::fw_row "$ts" "$client" \
|
||||
"$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc")" \
|
||||
"$w_client" "$w_dest"
|
||||
;;
|
||||
wg)
|
||||
IFS='|' read -r client endpoint event count <<< "$rest"
|
||||
ui::watch::wg_row "$ts" "$client" "$endpoint" "$event" \
|
||||
"$w_client" "$w_dest"
|
||||
;;
|
||||
esac
|
||||
done < <(echo "$merged_data" | sort -t'|' -k2,2)
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function cmd::logs::follow() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}"
|
||||
local fw_only="${4:-false}" wg_only="${5:-false}"
|
||||
|
||||
log::section "WireGuard Live Log (Ctrl+C to stop)"
|
||||
printf "\n"
|
||||
|
||||
local restricted_only=false blocked_only=false
|
||||
$fw_only && restricted_only=true
|
||||
$wg_only && blocked_only=true
|
||||
|
||||
monitor::live "$filter_name" "$filter_type" "" \
|
||||
"$blocked_only" "$restricted_only" "false" "false"
|
||||
}
|
||||
|
||||
function cmd::logs::remove() {
|
||||
local name="" type="" before="" force=false
|
||||
local fw_only=false wg_only=false all=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$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 ;;
|
||||
--help) cmd::logs::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! $all && [[ -z "$name" && -z "$before" ]]; then
|
||||
log::error "Specify --name, --before, or --all"
|
||||
cmd::logs::help
|
||||
return 1
|
||||
fi
|
||||
|
||||
local filter_ip=""
|
||||
if [[ -n "$name" ]]; then
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
filter_ip=$(peers::get_ip "$name")
|
||||
fi
|
||||
|
||||
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
|
||||
read -r -p "Remove ${desc}? [y/N] " confirm
|
||||
case "$confirm" in
|
||||
[yY]*) ;;
|
||||
*) log::info "Aborted"; return 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
local result
|
||||
result=$(json::remove_events_filtered \
|
||||
"$WG_EVENTS_LOG" "$FW_EVENTS_LOG" \
|
||||
"${name:-}" "${filter_ip:-}" \
|
||||
"$fw_only" "$wg_only" \
|
||||
"${before:-}")
|
||||
|
||||
local removed_wg removed_fw
|
||||
IFS="|" read -r removed_wg removed_fw <<< "$result"
|
||||
local total=$(( removed_wg + removed_fw ))
|
||||
|
||||
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::rotate() {
|
||||
local days=7 force=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--days) days="$2"; shift 2 ;;
|
||||
--force) force=true; shift ;;
|
||||
--help) cmd::logs::help; return ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
$force || {
|
||||
read -r -p "Remove log entries older than ${days} days? [y/N] " confirm
|
||||
case "$confirm" in
|
||||
[yY]*) ;;
|
||||
*) log::info "Aborted"; return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
local result
|
||||
result=$(json::remove_events_filtered \
|
||||
"$WG_EVENTS_LOG" "$FW_EVENTS_LOG" \
|
||||
"" "" "false" "false" "$days")
|
||||
|
||||
local removed_wg removed_fw
|
||||
IFS="|" read -r removed_wg removed_fw <<< "$result"
|
||||
local total=$(( removed_wg + removed_fw ))
|
||||
|
||||
if [[ "$total" -eq 0 ]]; then
|
||||
log::wg_warning "No log entries older than ${days} days"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::wg_success "Rotated ${total} entries older than ${days} days (wg: ${removed_wg}, fw: ${removed_fw})"
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/logs.sh — router only
|
||||
|
||||
function cmd::logs::on_load() {
|
||||
command::helpers "helpers.sh"
|
||||
command::define show "Show WireGuard and firewall logs" [*]
|
||||
command::define clean "Remove keepalive handshakes" [c]
|
||||
command::define remove "Remove log entries" [rm, del]
|
||||
command::define rotate "Remove entries older than N days"
|
||||
}
|
||||
|
||||
hook::on "command:help:logs" command::help::auto
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/remove.sh
|
||||
|
||||
function cmd::logs::remove::on_load() {
|
||||
flag::define --name value "Filter by peer name" label:name
|
||||
flag::define --since value "Remove entries since" label:time
|
||||
flag::define --fw bool "Remove firewall logs only"
|
||||
flag::define --wg bool "Remove WireGuard logs only"
|
||||
flag::define --force bool "Skip confirmation"
|
||||
}
|
||||
|
||||
function cmd::logs::remove::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local since; since=$(flag::value --since)
|
||||
local force=false fw=false wg=false
|
||||
flag::bool --force && force=true
|
||||
flag::bool --fw && fw=true
|
||||
flag::bool --wg && wg=true
|
||||
|
||||
cmd::logs::_remove_impl "$name" "$since" "$fw" "$wg" "$force"
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/rotate.sh
|
||||
|
||||
function cmd::logs::rotate::on_load() {
|
||||
flag::define --days value "Remove entries older than N days" default:30 type:int min:1 label:days
|
||||
flag::define --force bool "Skip confirmation"
|
||||
flag::define --dry-run bool "Show what would be removed"
|
||||
}
|
||||
|
||||
function cmd::logs::rotate::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local days; days=$(flag::value --days)
|
||||
local force=false dry_run=false
|
||||
flag::bool --force && force=true
|
||||
flag::bool --dry-run && dry_run=true
|
||||
|
||||
cmd::logs::_rotate_impl "$days" "$force" "$dry_run"
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/show.sh
|
||||
|
||||
FW_EVENTS_LOG="$(ctx::fw_events_log)"
|
||||
WG_EVENTS_LOG="$(ctx::events_log)"
|
||||
|
||||
function cmd::logs::show::on_load() {
|
||||
command::mixin json_output [section="Output"]
|
||||
|
||||
help::section "Filters"
|
||||
flag::define --name value "Filter by peer name" label:name section:Filters
|
||||
flag::define --type value "Filter by device type" label:type section:Filters
|
||||
flag::define --since value "Show events since (2h, 7d)" label:time section:Filters
|
||||
flag::define --service value "Filter by service/IP" label:service section:Filters
|
||||
flag::define --event value "Filter wg events" label:type section:Filters
|
||||
flag::define --fw bool "Show firewall drops only" section:Filters
|
||||
flag::define --wg bool "Show WireGuard events only" section:Filters
|
||||
flag::define --merged bool "Show all events interleaved" section:Filters
|
||||
|
||||
help::section "Output"
|
||||
flag::define --limit value "Max results per source" default:50 type:int min:1 section:Output
|
||||
flag::define --ascending bool "Sort ascending" section:Output
|
||||
flag::define --descending bool "Sort descending" section:Output
|
||||
flag::define --resolved bool "Resolve endpoints" section:Output
|
||||
flag::define --detailed bool "Show per-event detail" section:Output
|
||||
flag::define --raw bool "Skip service resolution" section:Output
|
||||
flag::define --follow bool "Follow live" section:Output
|
||||
|
||||
flag::exclusive --fw --wg
|
||||
flag::exclusive --ascending --descending
|
||||
}
|
||||
|
||||
function cmd::logs::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local name; name=$(flag::value --name)
|
||||
local type; type=$(flag::value --type)
|
||||
local limit; limit=$(flag::value --limit)
|
||||
local since; since=$(flag::value --since)
|
||||
local filter_service; filter_service=$(flag::value --service)
|
||||
local filter_event; filter_event=$(flag::value --event)
|
||||
local net_file=""
|
||||
|
||||
local fw_only=false wg_only=false merged=false
|
||||
local follow=false raw=false resolved=false detailed=false
|
||||
flag::bool --fw && fw_only=true
|
||||
flag::bool --wg && wg_only=true
|
||||
flag::bool --merged && merged=true
|
||||
flag::bool --follow && follow=true
|
||||
flag::bool --raw && raw=true
|
||||
flag::bool --resolved && resolved=true
|
||||
flag::bool --detailed && detailed=true
|
||||
|
||||
local sort_order="desc"
|
||||
flag::bool --ascending && sort_order="asc"
|
||||
flag::bool --descending && sort_order="desc"
|
||||
|
||||
local collapse=1
|
||||
$detailed && collapse=0
|
||||
|
||||
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
|
||||
|
||||
$fw_only && $wg_only && fw_only=false && wg_only=false
|
||||
|
||||
if $follow; then
|
||||
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
|
||||
return
|
||||
fi
|
||||
|
||||
$raw || net_file="$(ctx::net)"
|
||||
|
||||
local filter_dest_ip="" filter_dest_port=""
|
||||
if [[ -n "$filter_service" ]]; then
|
||||
if [[ "$filter_service" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(:[0-9]+)?$ ]]; then
|
||||
filter_dest_ip="${filter_service%%:*}"
|
||||
local maybe_port="${filter_service##*:}"
|
||||
[[ "$maybe_port" != "$filter_dest_ip" ]] && filter_dest_port="$maybe_port"
|
||||
else
|
||||
local svc_resolved
|
||||
svc_resolved=$(net::resolve "$filter_service" 2>/dev/null | head -1)
|
||||
if [[ -n "$svc_resolved" ]]; then
|
||||
filter_dest_ip="${svc_resolved%%:*}"
|
||||
local rest="${svc_resolved#*:}"
|
||||
[[ "$rest" != "$filter_dest_ip" ]] && filter_dest_port="${rest%%:*}"
|
||||
else
|
||||
log::error "Service not found: ${filter_service}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if $merged; then
|
||||
log::section "WireGuard Activity Log"
|
||||
printf "\n"
|
||||
cmd::logs::show_merged "$filter_ip" "$name" "$type" "$limit" "$net_file" "$since"
|
||||
return
|
||||
fi
|
||||
|
||||
local fw_output="" wg_output=""
|
||||
|
||||
$wg_only || fw_output=$(cmd::logs::show_fw_events \
|
||||
"$filter_ip" "$name" "$type" "$limit" "$net_file" \
|
||||
"$collapse" "$since" "$filter_dest_ip" "$filter_dest_port" "$sort_order" "$resolved")
|
||||
|
||||
$fw_only || wg_output=$(cmd::logs::show_wg_events \
|
||||
"$filter_ip" "$name" "$type" "$limit" \
|
||||
"$collapse" "$since" "$filter_event" "$sort_order" "$resolved")
|
||||
|
||||
if [[ -z "$(echo "$fw_output" | tr -d '[:space:]')" && \
|
||||
-z "$(echo "$wg_output" | tr -d '[:space:]')" ]]; then
|
||||
log::wg_warning "No logs found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::section "WireGuard Activity Log"
|
||||
printf "\n"
|
||||
|
||||
if [[ -n "$fw_output" && -n "$wg_output" ]]; then
|
||||
printf "%s\n\n" "$fw_output"
|
||||
printf "%s\n" "$wg_output"
|
||||
elif [[ -n "$fw_output" ]]; then
|
||||
printf "%s\n" "$fw_output"
|
||||
else
|
||||
printf "%s\n" "$wg_output"
|
||||
fi
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/peer/peer.sh — router only
|
||||
|
||||
function cmd::peer::on_load() {
|
||||
command::define update-dns "Update DNS servers in peer configs" [dns]
|
||||
command::define update-tunnel "Update tunnel mode for peer configs" [tunnel]
|
||||
}
|
||||
|
||||
hook::on "command:help:peer" command::help::auto
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/peer/update-dns.sh
|
||||
|
||||
function cmd::peer::update_dns::on_load() {
|
||||
help::section "Target"
|
||||
flag::define --name value "Peer name" label:name section:Target
|
||||
flag::define --type value "Filter by type" label:type section:Target
|
||||
flag::define --all bool "Update all peers" section:Target
|
||||
|
||||
help::section "DNS"
|
||||
flag::define --dns value "Primary DNS server" label:ip section:DNS
|
||||
flag::define --fallback-dns value "Fallback DNS servers" label:ips section:DNS
|
||||
}
|
||||
|
||||
function cmd::peer::update_dns::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local name; name=$(flag::value --name)
|
||||
local type; type=$(flag::value --type)
|
||||
local dns; dns=$(flag::value --dns)
|
||||
local fallback_dns; fallback_dns=$(flag::value --fallback-dns)
|
||||
local all=false
|
||||
flag::bool --all && all=true
|
||||
|
||||
[[ -z "$name" && "$all" == "false" ]] && \
|
||||
log::error "Specify --name or --all" && return 1
|
||||
|
||||
local primary="${dns:-$(config::dns)}"
|
||||
local fallback="${fallback_dns:-$(config::dns_fallback)}"
|
||||
local dns_string
|
||||
if [[ -n "$fallback" ]]; then
|
||||
dns_string="${primary}, ${fallback}"
|
||||
else
|
||||
dns_string="$primary"
|
||||
fi
|
||||
|
||||
local -a peers=()
|
||||
if $all; then
|
||||
while IFS= read -r conf; do
|
||||
peers+=("$(basename "$conf" .conf)")
|
||||
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
||||
else
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
peers=("$name")
|
||||
fi
|
||||
|
||||
local updated=0
|
||||
for peer_name in "${peers[@]}"; do
|
||||
local conf
|
||||
conf="$(ctx::clients)/${peer_name}.conf"
|
||||
[[ ! -f "$conf" ]] && continue
|
||||
if grep -q "^DNS" "$conf"; then
|
||||
sed -i "s|^DNS = .*|DNS = ${dns_string}|" "$conf"
|
||||
else
|
||||
sed -i "/^Address/a DNS = ${dns_string}" "$conf"
|
||||
fi
|
||||
(( updated++ )) || true
|
||||
log::debug "Updated DNS for: ${peer_name}"
|
||||
done
|
||||
|
||||
log::wg_success "Updated DNS to '${dns_string}' for ${updated} peer(s)"
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/peer/update-tunnel.sh
|
||||
|
||||
function cmd::peer::update_tunnel::on_load() {
|
||||
help::section "Target"
|
||||
flag::define --name value "desc" label:name section:Filters
|
||||
flag::define --type value "Filter by type" label:type section:Target
|
||||
flag::define --all bool "Update all peers" section:Target
|
||||
|
||||
help::section "Options"
|
||||
flag::define --mode value "Tunnel mode" label:mode required choices:split,full section:Options
|
||||
flag::define --force bool "Skip confirmation for --all" section:Options
|
||||
}
|
||||
|
||||
function cmd::peer::update_tunnel::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local name; name=$(flag::value --name)
|
||||
local type; type=$(flag::value --type)
|
||||
local mode; mode=$(flag::value --mode)
|
||||
local all=false force=false
|
||||
flag::bool --all && all=true
|
||||
flag::bool --force && force=true
|
||||
|
||||
[[ -z "$name" && "$all" == "false" ]] && \
|
||||
log::error "Specify --name or --all" && return 1
|
||||
|
||||
local allowed_ips
|
||||
allowed_ips=$(config::allowed_ips_for "$mode")
|
||||
|
||||
local -a peers=()
|
||||
if $all; then
|
||||
if ! $force; then
|
||||
read -r -p "Update tunnel mode to '${mode}' for ALL peers? [y/N] " confirm
|
||||
case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac
|
||||
fi
|
||||
while IFS= read -r conf; do
|
||||
peers+=("$(basename "$conf" .conf)")
|
||||
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
||||
else
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
peers=("$name")
|
||||
fi
|
||||
|
||||
local updated=0
|
||||
for peer_name in "${peers[@]}"; do
|
||||
local conf
|
||||
conf="$(ctx::clients)/${peer_name}.conf"
|
||||
[[ ! -f "$conf" ]] && continue
|
||||
sed -i "s|^AllowedIPs = .*|AllowedIPs = ${allowed_ips}|" "$conf"
|
||||
(( updated++ )) || true
|
||||
log::debug "Updated tunnel for: ${peer_name}"
|
||||
done
|
||||
|
||||
log::wg_success "Updated tunnel to '${mode}' (${allowed_ips}) for ${updated} peer(s)"
|
||||
log::wg "Peers must reconnect to apply the new tunnel mode"
|
||||
}
|
||||
|
|
@ -199,8 +199,9 @@ function cmd::test::section_groups() {
|
|||
test::section "Groups"
|
||||
cmd::test::run_cmd "group list" "Groups" group list
|
||||
cmd::test::run_cmd "group show --name family" "Peers:" group show --name family
|
||||
cmd::test::run_cmd "group list --json" '"name":' group list --json
|
||||
cmd::test::run_cmd "group list --json" '"command":"groups"' group list --json
|
||||
cmd::test::run_cmd "group list --json" '"groups":' group list --json
|
||||
cmd::test::run_cmd "group list --json" '"groups":' group list --json
|
||||
cmd::test::run_cmd "group --json peer_count" '"peer_count":' group list --json
|
||||
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
||||
}
|
||||
|
||||
|
|
@ -379,9 +380,9 @@ function cmd::test::section_identity() {
|
|||
function cmd::test::section_activity() {
|
||||
test::section "Activity"
|
||||
cmd::test::run_cmd "activity" "Activity" activity
|
||||
cmd::test::run_cmd "activity --json" '"command":"activity"' activity --json
|
||||
cmd::test::run_cmd "activity --json has data" '"data":' activity --json
|
||||
cmd::test::run_cmd "activity --json has rx" '"rx":' activity --json
|
||||
cmd::test::run_cmd "activity --json" '"peers":' activity --json
|
||||
cmd::test::run_cmd "activity --json services" '"services":' activity --json
|
||||
cmd::test::run_cmd "activity --json rx" '"rx":' activity --json
|
||||
}
|
||||
|
||||
function cmd::test::section_policy() {
|
||||
|
|
|
|||
|
|
@ -1,180 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/unblock/helpers.sh
|
||||
|
||||
function cmd::unblock::_impl() {
|
||||
local name="$1" identity="$2" type="$3" reason="$4"
|
||||
local all="$5" quiet="$6" force="$7"
|
||||
local -n _ips_ref="$8"
|
||||
local -n _subnets_ref="$9"
|
||||
local -n _ports_ref="${10}"
|
||||
local -n _services_ref="${11}"
|
||||
|
||||
if [[ -n "$identity" ]]; then
|
||||
cmd::unblock::_unblock_identity "$identity" "$quiet" || return 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ -z "$name" ]] && {
|
||||
log::error "Missing required flag: --name or --identity"
|
||||
return 1
|
||||
}
|
||||
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
|
||||
if ! peers::is_blocked "$name" && ! block::has_file "$name"; then
|
||||
log::wg_warning "Client is not blocked: ${name}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ${#_ips_ref[@]} -eq 0 && ${#_subnets_ref[@]} -eq 0 && \
|
||||
${#_ports_ref[@]} -eq 0 && ${#_services_ref[@]} -eq 0 ]]; then
|
||||
all=true
|
||||
fi
|
||||
|
||||
local client_ip
|
||||
client_ip=$(peers::get_ip "$name") || return 1
|
||||
|
||||
if [[ "$all" == "true" ]]; then
|
||||
cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet"
|
||||
cmd::unblock::_record_history "$name" "manual" "$reason"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for ip in "${_ips_ref[@]:-}"; do
|
||||
fw::unblock_ip "$client_ip" "$ip"
|
||||
block::remove_rule "$name" "ip" "$ip"
|
||||
$quiet || log::wg_success "${ip} has been unblocked for ${name}"
|
||||
done
|
||||
|
||||
for subnet in "${_subnets_ref[@]:-}"; do
|
||||
fw::unblock_subnet "$client_ip" "$subnet"
|
||||
block::remove_rule "$name" "subnet" "$subnet"
|
||||
$quiet || log::wg_success "${subnet} has been unblocked for ${name}"
|
||||
done
|
||||
|
||||
for entry in "${_ports_ref[@]:-}"; do
|
||||
local target port proto
|
||||
IFS=":" read -r target port proto <<< "$entry"
|
||||
proto="${proto:-tcp}"
|
||||
fw::unblock_port "$client_ip" "$target" "$port" "$proto"
|
||||
block::remove_rule "$name" "port" "$target" "$port" "$proto"
|
||||
$quiet || log::wg_success "${target}:${port}:${proto} has been unblocked for ${name}"
|
||||
done
|
||||
|
||||
for svc in "${_services_ref[@]:-}"; do
|
||||
local resolved_lines=()
|
||||
mapfile -t resolved_lines < <(net::resolve "$svc" 2>/dev/null)
|
||||
if [[ ${#resolved_lines[@]} -eq 0 ]]; then
|
||||
log::error "Service not found: ${svc}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local is_blocked=false
|
||||
for resolved in "${resolved_lines[@]}"; do
|
||||
if [[ "$resolved" == *:*:* ]]; then
|
||||
local b_ip b_port b_proto
|
||||
IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
|
||||
fw::has_block_rule "$client_ip" "$b_ip" "$b_port" "$b_proto" 2>/dev/null && \
|
||||
{ is_blocked=true; break; }
|
||||
else
|
||||
fw::has_block_rule "$client_ip" "$resolved" 2>/dev/null && \
|
||||
{ is_blocked=true; break; }
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $is_blocked; then
|
||||
$quiet || log::wg_warning "${svc} is not blocked for ${name}"
|
||||
continue
|
||||
fi
|
||||
|
||||
for resolved in "${resolved_lines[@]}"; do
|
||||
if [[ "$resolved" == *:*:* ]]; then
|
||||
local b_ip b_port b_proto
|
||||
IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
|
||||
fw::unblock_port "$client_ip" "$b_ip" "$b_port" "$b_proto"
|
||||
block::remove_rule "$name" "port" "$b_ip" "$b_port" "$b_proto"
|
||||
else
|
||||
fw::unblock_ip "$client_ip" "$resolved"
|
||||
block::remove_rule "$name" "ip" "$resolved"
|
||||
fi
|
||||
done
|
||||
|
||||
$quiet || log::wg_success "${svc} has been unblocked for ${name}"
|
||||
done
|
||||
|
||||
block::cleanup "$name"
|
||||
cmd::unblock::_record_history "$name" "manual" "$reason"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::unblock::_unblock_identity() {
|
||||
local identity_name="${1:-}" quiet="${2:-false}"
|
||||
|
||||
identity::require_exists "$identity_name" || return 1
|
||||
identity::require_has_peers "$identity_name" || return 1
|
||||
|
||||
local peers unblocked=0 skipped=0
|
||||
peers=$(identity::peers "$identity_name")
|
||||
|
||||
while IFS= read -r peer_name; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
if ! peers::is_blocked "$peer_name" && ! block::has_file "$peer_name"; then
|
||||
skipped=$(( skipped + 1 ))
|
||||
continue
|
||||
fi
|
||||
local client_ip
|
||||
client_ip=$(peers::get_ip "$peer_name") || continue
|
||||
|
||||
if cmd::unblock::_unblock_all "$peer_name" "$client_ip" true; then
|
||||
unblocked=$(( unblocked + 1 ))
|
||||
else
|
||||
skipped=$(( skipped + 1 ))
|
||||
fi
|
||||
done <<< "$peers"
|
||||
|
||||
if [[ $unblocked -eq 0 ]]; then
|
||||
log::wg_warning "No peers were blocked for identity '${identity_name}'"
|
||||
elif [[ $skipped -gt 0 ]]; then
|
||||
log::ok "Unblocked ${unblocked} peer(s) for identity '${identity_name}' (${skipped} were not blocked)"
|
||||
else
|
||||
log::ok "Unblocked ${unblocked} peer(s) for identity '${identity_name}'"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::unblock::_unblock_all() {
|
||||
local name="${1:?}" client_ip="${2:?}" quiet="${3:-false}"
|
||||
|
||||
block::set_direct "$name" "$client_ip" "false"
|
||||
block::clear_full_block "$name"
|
||||
block::restore_peer "$name" "$client_ip"
|
||||
block::cleanup "$name"
|
||||
|
||||
local rule
|
||||
rule=$(peers::get_meta "$name" "rule")
|
||||
if [[ -n "$rule" ]] && rule::exists "$rule"; then
|
||||
rule::apply "$rule" "$client_ip" "$name"
|
||||
fi
|
||||
|
||||
local groups
|
||||
groups=$(block::get_groups "$name")
|
||||
if [[ -n "$groups" ]]; then
|
||||
log::wg_warning "${name} was blocked by group(s): ${groups} — unblocking anyway"
|
||||
fi
|
||||
|
||||
$quiet || log::wg_success "${name} has been unblocked."
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::unblock::_record_history() {
|
||||
local name="${1:-}" unblocked_by="${2:-manual}" reason="${3:-}"
|
||||
|
||||
json::block_history_unblock \
|
||||
"$(ctx::block_history)" \
|
||||
"$name" \
|
||||
"$unblocked_by" \
|
||||
"$reason" \
|
||||
2>/dev/null > /dev/null || true
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/unblock/show.sh
|
||||
|
||||
function cmd::unblock::show::on_load() {
|
||||
help::section "Target"
|
||||
flag::define --name value "Peer name to unblock" label:name section:Target
|
||||
flag::define --identity value "Unblock all peers in identity" label:identity section:Target
|
||||
flag::define --type value "Filter by device type" label:type section:Target
|
||||
|
||||
help::section "Rules"
|
||||
flag::define --ip[] "Unblock specific IP" label:ip section:Rules
|
||||
flag::define --subnet[] "Unblock specific subnet" label:subnet section:Rules
|
||||
flag::define --port[] "Unblock specific port (ip:port:proto)" label:port section:Rules
|
||||
flag::define --service[] "Unblock by service name" label:service section:Rules
|
||||
flag::define --all bool "Unblock all rules" section:Rules
|
||||
|
||||
help::section "Options"
|
||||
flag::define --reason value "Reason for unblock (recorded in history)" label:reason section:Options
|
||||
flag::define --force bool "Skip confirmation" section:Options
|
||||
flag::define --quiet bool "Suppress output" section:Options
|
||||
|
||||
flag::exclusive --name --identity
|
||||
}
|
||||
|
||||
function cmd::unblock::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local name; name=$(flag::value --name)
|
||||
local identity; identity=$(flag::value --identity)
|
||||
local type; type=$(flag::value --type)
|
||||
local reason; reason=$(flag::value --reason)
|
||||
local all=false quiet=false force=false
|
||||
flag::bool --all && all=true
|
||||
flag::bool --quiet && quiet=true
|
||||
flag::bool --force && force=true
|
||||
|
||||
local -a ips=() subnets=() ports=() services=()
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && ips+=("$v"); done < <(flag::array --ip)
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && subnets+=("$v"); done < <(flag::array --subnet)
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && ports+=("$v"); done < <(flag::array --port)
|
||||
while IFS= read -r v; do [[ -n "$v" ]] && services+=("$v"); done < <(flag::array --service)
|
||||
|
||||
if [[ -z "$name" && -z "$identity" ]]; then
|
||||
log::error "Missing required flag: --name or --identity"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cmd::unblock::_impl \
|
||||
"$name" "$identity" "$type" "$reason" \
|
||||
"$all" "$quiet" "$force" \
|
||||
ips subnets ports services
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/unblock/unblock.sh — router only
|
||||
|
||||
function cmd::unblock::on_load() {
|
||||
command::helpers "helpers.sh"
|
||||
command::define show "Unblock a peer or service" [*]
|
||||
}
|
||||
|
||||
hook::on "command:help:unblock" command::help::auto
|
||||
22
core.sh
Normal file
22
core.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Core Bootstrap
|
||||
# ============================================
|
||||
|
||||
WGCTL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "${WGCTL_DIR}/core/log.sh"
|
||||
source "${WGCTL_DIR}/core/context.sh"
|
||||
source "${WGCTL_DIR}/core/utils.sh"
|
||||
source "${WGCTL_DIR}/core/module.sh"
|
||||
source "${WGCTL_DIR}/core/command.sh"
|
||||
source "${WGCTL_DIR}/core/command_mixins.sh"
|
||||
source "${WGCTL_DIR}/core/flag.sh"
|
||||
source "${WGCTL_DIR}/core/json.sh"
|
||||
source "${WGCTL_DIR}/core/ui.sh"
|
||||
source "${WGCTL_DIR}/core/color.sh"
|
||||
source "${WGCTL_DIR}/core/fmt.sh"
|
||||
source "${WGCTL_DIR}/core/test/test.sh"
|
||||
|
||||
command::_load_mixins
|
||||
BIN
core/__pycache__/json_helper.cpython-311.pyc
Normal file
BIN
core/__pycache__/json_helper.cpython-311.pyc
Normal file
Binary file not shown.
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
_APP_CORE_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# ============================================
|
||||
# Core Bootstrap
|
||||
# ============================================
|
||||
|
||||
source "${_APP_CORE_DIR}/context.sh"
|
||||
source "${_APP_CORE_DIR}/json.sh"
|
||||
|
||||
export PYTHONPATH="$(ctx::core):${PYTHONPATH:-}"
|
||||
|
||||
function app::_load_mixins() {
|
||||
local mixin_file
|
||||
for mixin_file in "${WGCTL_DIR}/commands/mixins/"*.mixin.sh; do
|
||||
[[ -f "$mixin_file" ]] && source "$mixin_file"
|
||||
done
|
||||
}
|
||||
app::_load_mixins
|
||||
|
|
@ -15,7 +15,8 @@ declare -a _ACTIVE_MIXINS=()
|
|||
function command::_load_mixins() {
|
||||
local mixin_file
|
||||
local -a mixin_paths=(
|
||||
"${_FRAMEWORK_DIR}/mixins/"*.mixin.sh
|
||||
"${WGCTL_DIR}/core/mixins/"*.mixin.sh
|
||||
"${WGCTL_DIR}/commands/mixins/"*.mixin.sh
|
||||
)
|
||||
for mixin_file in "${mixin_paths[@]:-}"; do
|
||||
[[ -f "$mixin_file" ]] && source "$mixin_file"
|
||||
|
|
@ -181,7 +182,6 @@ declare -gA _FLAG_EXCLUSIVE_GROUPS=()
|
|||
function flag::exclusive() {
|
||||
local cmd="${_CURRENT_LOADING_CMD:-}"
|
||||
[[ -z "$cmd" ]] && return 0
|
||||
cmd="${cmd%%::*}"
|
||||
|
||||
# Join flags with comma as one group
|
||||
local group
|
||||
|
|
@ -4,8 +4,11 @@
|
|||
# Static Context — resolved once at source time
|
||||
# ============================================
|
||||
|
||||
_CTX_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
_CTX_WG="/etc/wireguard"
|
||||
_CTX_WGCTL="/etc/wireguard/wgctl"
|
||||
_CTX_CORE="${_CTX_ROOT}/core"
|
||||
_CTX_MODULES="${_CTX_ROOT}/modules"
|
||||
_CTX_COMMANDS="${_CTX_ROOT}/commands"
|
||||
_CTX_CLIENTS="${_CTX_WG}/clients"
|
||||
|
||||
# ── Directory layout ──────────────────────────────────
|
||||
|
|
@ -14,10 +17,10 @@ _CTX_CLIENTS="${_CTX_WG}/clients"
|
|||
# data/ ← all persistent data (rules, identities, etc.)
|
||||
# daemon/ ← runtime files (logs, caches)
|
||||
|
||||
_CTX_WGCTL_ARTIFACT="${_CTX_WG}/.wgctl"
|
||||
_CTX_CONFIG="${_CTX_WGCTL_ARTIFACT}/config"
|
||||
_CTX_DATA="${_CTX_WGCTL_ARTIFACT}/data"
|
||||
_CTX_DAEMON="${_CTX_WGCTL_ARTIFACT}/daemon"
|
||||
_CTX_WGCTL="${_CTX_WG}/.wgctl"
|
||||
_CTX_CONFIG="${_CTX_WGCTL}/config"
|
||||
_CTX_DATA="${_CTX_WGCTL}/data"
|
||||
_CTX_DAEMON="${_CTX_WGCTL}/daemon"
|
||||
|
||||
# ── Data subdirs ──────────────────────────────────────
|
||||
_CTX_RULES="${_CTX_DATA}/rules"
|
||||
|
|
@ -41,11 +44,15 @@ _CTX_CONFIG_FILE="${_CTX_CONFIG}/wgctl.json"
|
|||
# Accessors
|
||||
# ============================================
|
||||
|
||||
function ctx::root() { echo "$_CTX_ROOT"; }
|
||||
function ctx::core() { echo "$_CTX_CORE"; }
|
||||
function ctx::modules() { echo "$_CTX_MODULES"; }
|
||||
function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
||||
function ctx::wg() { echo "$_CTX_WG"; }
|
||||
function ctx::clients() { echo "$_CTX_CLIENTS"; }
|
||||
|
||||
# Top-level dirs
|
||||
function ctx::wgctl() { echo "$_CTX_WGCTL_ARTIFACT"; } # needs to change to ctx::wgctl_artifact or ctx::artifact
|
||||
function ctx::wgctl() { echo "$_CTX_WGCTL"; }
|
||||
function ctx::config() { echo "$_CTX_CONFIG"; }
|
||||
function ctx::data() { echo "$_CTX_DATA"; }
|
||||
function ctx::daemon() { echo "$_CTX_DAEMON"; }
|
||||
|
|
@ -76,9 +83,9 @@ function ctx::endpoint_cache() { echo "${_CTX_DAEMON}/endpoint_cache.json";
|
|||
function ctx::accept_events_log() { echo "${_CTX_DAEMON}/accept_events.log"; }
|
||||
|
||||
# Tool paths
|
||||
function ctx::json_helper() { echo "$(ctx::app)/json_helper.py"; }
|
||||
function ctx::monitor_script() { echo "${_CTX_WGCTL}/daemon/wgctl-monitor.py"; }
|
||||
function ctx::lib() { echo "${_CTX_WGCTL}/core/lib"; }
|
||||
function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
||||
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
||||
function ctx::lib() { echo "${_CTX_CORE}/lib"; }
|
||||
|
||||
function ctx::block_history() { echo "${_CTX_DATA}/block-history"; }
|
||||
|
||||
12
core/core.sh
12
core/core.sh
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Core Bootstrap
|
||||
# ============================================
|
||||
|
||||
WGCTL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FRAMEWORK_DIR="$WGCTL_DIR/core/framework"
|
||||
APP_CORE_DIR="$WGCTL_DIR/core/app"
|
||||
|
||||
source "${FRAMEWORK_DIR}/core.sh"
|
||||
source "${APP_CORE_DIR}/core.sh"
|
||||
|
|
@ -1,429 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/command.sh
|
||||
#
|
||||
# Command definition, routing, and lazy loading.
|
||||
#
|
||||
# Usage in on_load:
|
||||
# command::define show "Show logs" [*]
|
||||
# command::define clean "Remove keepalives" [c, dump]
|
||||
# command::define watch "Watch live" [w, follow]
|
||||
#
|
||||
# Routing:
|
||||
# wgctl logs → cmd::logs::show::run (default)
|
||||
# wgctl logs clean → cmd::logs::clean::run
|
||||
# wgctl logs c → cmd::logs::clean::run (alias)
|
||||
# wgctl logs --fw → cmd::logs::show::run (default, flag passed through)
|
||||
|
||||
# ── Storage ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# Command definitions: "cmd:subcmd" → "desc|aliases|is_default"
|
||||
declare -gA _COMMAND_DEFS=()
|
||||
|
||||
# Alias map: "cmd:alias" → "subcmd"
|
||||
declare -gA _COMMAND_ALIASES_MAP=()
|
||||
|
||||
# Default subcommand per command: "cmd" → "subcmd"
|
||||
declare -gA _COMMAND_DEFAULT=()
|
||||
|
||||
# Currently loading command context (set by load_command)
|
||||
declare -g _CURRENT_LOADING_CMD=""
|
||||
|
||||
# Currently executing command context (set by command::run)
|
||||
declare -g _CURRENT_COMMAND=""
|
||||
|
||||
# ── Definition ────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::define <subcmd> "description" [*] or [alias1, alias2]
|
||||
# [*] marks as default subcommand
|
||||
function command::define() {
|
||||
local subcmd="${1:-}"
|
||||
local desc="${2:-}"
|
||||
local meta="${3:-}" # [*] or [alias1, alias2] or [*, alias1]
|
||||
local cmd="${_CURRENT_LOADING_CMD:-}"
|
||||
|
||||
[[ -z "$cmd" || -z "$subcmd" ]] && return 1
|
||||
|
||||
local is_default=false
|
||||
local aliases=""
|
||||
|
||||
if [[ -n "$meta" ]]; then
|
||||
# Strip brackets
|
||||
local inner="${meta#[}"
|
||||
inner="${inner%]}"
|
||||
|
||||
# Check for * (default marker)
|
||||
if [[ "$inner" == *"*"* ]]; then
|
||||
is_default=true
|
||||
inner="${inner//\*/}"
|
||||
inner="${inner//,/}"
|
||||
inner="${inner// /}"
|
||||
fi
|
||||
|
||||
# Remaining are aliases
|
||||
aliases="$inner"
|
||||
# Clean up leading/trailing commas and spaces
|
||||
aliases=$(echo "$aliases" | sed 's/^[, ]*//' | sed 's/[, ]*$//' | tr -s ' ,')
|
||||
fi
|
||||
|
||||
# Store definition: desc|aliases|is_default
|
||||
_COMMAND_DEFS["${cmd}:${subcmd}"]="${desc}|${aliases}|${is_default}"
|
||||
|
||||
# Register as default
|
||||
if $is_default; then
|
||||
_COMMAND_DEFAULT["$cmd"]="$subcmd"
|
||||
fi
|
||||
|
||||
# Register aliases
|
||||
if [[ -n "$aliases" ]]; then
|
||||
local alias
|
||||
# Split aliases on comma or space
|
||||
while IFS=',' read -ra alias_list; do
|
||||
for alias in "${alias_list[@]}"; do
|
||||
alias="${alias// /}"
|
||||
[[ -n "$alias" ]] && _COMMAND_ALIASES_MAP["${cmd}:${alias}"]="$subcmd"
|
||||
done
|
||||
done <<< "$aliases"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Routing ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::route <cmd> <args...>
|
||||
# Determines which subcommand to run and routes to it.
|
||||
# Returns: sets _ROUTED_SUBCMD and _ROUTED_ARGS
|
||||
declare -g _ROUTED_SUBCMD=""
|
||||
declare -ga _ROUTED_ARGS=()
|
||||
|
||||
function command::route() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
local -a args=("$@")
|
||||
|
||||
_ROUTED_SUBCMD=""
|
||||
_ROUTED_ARGS=()
|
||||
|
||||
# Check if _COMMAND_DEFS has any entries for this cmd
|
||||
local has_defs=false
|
||||
local key
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && has_defs=true && break
|
||||
done
|
||||
|
||||
if ! $has_defs; then
|
||||
# No subcommands defined — run directly
|
||||
_ROUTED_SUBCMD=""
|
||||
_ROUTED_ARGS=("${args[@]:-}")
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Find first non-flag arg
|
||||
local first_nonflag="" first_nonflag_idx=-1
|
||||
local i
|
||||
for (( i=0; i<${#args[@]}; i++ )); do
|
||||
if [[ "${args[$i]}" != --* && "${args[$i]}" != -* ]]; then
|
||||
first_nonflag="${args[$i]}"
|
||||
first_nonflag_idx=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
local matched_subcmd=""
|
||||
|
||||
if [[ -n "$first_nonflag" ]]; then
|
||||
# Check direct match
|
||||
if [[ -n "${_COMMAND_DEFS[${cmd}:${first_nonflag}]+x}" ]]; then
|
||||
matched_subcmd="$first_nonflag"
|
||||
# Check alias
|
||||
elif [[ -n "${_COMMAND_ALIASES_MAP[${cmd}:${first_nonflag}]+x}" ]]; then
|
||||
matched_subcmd="${_COMMAND_ALIASES_MAP[${cmd}:${first_nonflag}]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$matched_subcmd" ]]; then
|
||||
# Remove matched subcmd from args
|
||||
local -a new_args=()
|
||||
for (( i=0; i<${#args[@]}; i++ )); do
|
||||
[[ $i -eq $first_nonflag_idx ]] && continue
|
||||
new_args+=("${args[$i]}")
|
||||
done
|
||||
_ROUTED_SUBCMD="$matched_subcmd"
|
||||
_ROUTED_ARGS=("${new_args[@]:-}")
|
||||
else
|
||||
# Use default subcommand
|
||||
local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
|
||||
_ROUTED_SUBCMD="$default_subcmd"
|
||||
_ROUTED_ARGS=("${args[@]:-}")
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Loading ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::load_subcmd <cmd> <subcmd>
|
||||
# Lazy-loads a subcommand's file and calls its on_load
|
||||
function command::load_subcmd() {
|
||||
local cmd="$1" subcmd="$2"
|
||||
# log::debug "load_subcmd: cmd=$cmd subcmd=$subcmd"
|
||||
[[ -z "$cmd" || -z "$subcmd" ]] && return 1
|
||||
|
||||
# Determine file path — commands/<cmd>/<subcmd>.sh
|
||||
local cmd_dir
|
||||
cmd_dir="${WGCTL_DIR}/commands/${cmd}"
|
||||
|
||||
local subcmd_file="${cmd_dir}/${subcmd}.sh"
|
||||
# log::debug "looking for: $subcmd_file exists=$([ -f "$subcmd_file" ] && echo yes || echo no)"
|
||||
[[ ! -f "$subcmd_file" ]] && return 1
|
||||
|
||||
local fn_subcmd="${subcmd//-/_}"
|
||||
local on_load_fn="cmd::${cmd}::${fn_subcmd}::on_load"
|
||||
|
||||
_CURRENT_LOADING_CMD="${cmd}::${subcmd}"
|
||||
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
||||
source "$subcmd_file"
|
||||
|
||||
# log::debug "sourced: $subcmd_file exit=$?"
|
||||
# log::debug "on_load_fn=$on_load_fn exists=$(declare -f "$on_load_fn" &>/dev/null && echo yes || echo no)"
|
||||
|
||||
# log::debug "load_subcmd: calling on_load, _CURRENT_COMMAND=$_CURRENT_COMMAND _CURRENT_LOADING_CMD=$_CURRENT_LOADING_CMD"
|
||||
if declare -f "$on_load_fn" &>/dev/null; then
|
||||
"$on_load_fn"
|
||||
fi
|
||||
|
||||
_CURRENT_LOADING_CMD=""
|
||||
}
|
||||
|
||||
function command::helpers() {
|
||||
local file="${1:-}"
|
||||
local cmd="${_CURRENT_LOADING_CMD:-}"
|
||||
[[ -z "$file" || -z "$cmd" ]] && return 1
|
||||
local cmd_name="${cmd%%::*}"
|
||||
local path="$(ctx::commands)/${cmd_name}/${file}"
|
||||
[[ -f "$path" ]] && source "$path"
|
||||
}
|
||||
|
||||
# ── Run ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::run_routed <cmd> <subcmd> <args...>
|
||||
# Runs a subcommand after routing is resolved.
|
||||
function command::run_routed() {
|
||||
local cmd="$1" subcmd="$2"
|
||||
shift 2
|
||||
local -a args=("$@")
|
||||
|
||||
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
||||
|
||||
# Apply command defaults (only for default subcommand)
|
||||
local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
|
||||
local -a default_args=()
|
||||
if [[ "$subcmd" == "$default_subcmd" || -z "$default_subcmd" ]]; then
|
||||
local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
|
||||
if [[ -n "$defaults" ]]; then
|
||||
read -ra default_args <<< "$defaults"
|
||||
fi
|
||||
fi
|
||||
|
||||
local -a user_args=()
|
||||
[[ ${#args[@]} -gt 0 ]] && user_args=("${args[@]}")
|
||||
|
||||
# Resolve exclusive group conflicts
|
||||
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
|
||||
|
||||
# Combine args
|
||||
local -a final_args=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && final_args+=("$_d")
|
||||
done
|
||||
for _u in "${user_args[@]:-}"; do
|
||||
[[ -n "$_u" ]] && final_args+=("$_u")
|
||||
done
|
||||
|
||||
# Preprocess mixin flags (--json, --no-color etc)
|
||||
command::_preprocess_flags final_args
|
||||
|
||||
# Run
|
||||
local fn_subcmd="${subcmd//-/_}"
|
||||
local run_fn="cmd::${cmd}::${fn_subcmd}::run"
|
||||
if declare -f "$run_fn" &>/dev/null; then
|
||||
if [[ ${#final_args[@]} -gt 0 ]]; then
|
||||
"$run_fn" "${final_args[@]}"
|
||||
else
|
||||
"$run_fn"
|
||||
fi
|
||||
else
|
||||
log::error "Run function not found: ${run_fn}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::has_subcmds <cmd>
|
||||
# Returns 0 if command has defined subcommands
|
||||
function command::has_subcmds() {
|
||||
local cmd="$1" key
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# command::subcmds <cmd>
|
||||
# Prints list of subcommand names
|
||||
function command::subcmds() {
|
||||
local cmd="$1" key
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && echo "${key#${cmd}:}"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Command Registry
|
||||
# ============================================
|
||||
|
||||
declare -A _LOADED_COMMANDS=()
|
||||
|
||||
readonly _COMMAND_NAMESPACE="cmd"
|
||||
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
|
||||
# ============================================
|
||||
# Helpers
|
||||
# ============================================
|
||||
|
||||
function command::loaded() { [[ -n "${_LOADED_COMMANDS["$1"]:-}" ]]; }
|
||||
|
||||
# Convert path-style name to namespace
|
||||
# e.g. service/wireguard -> service::wireguard
|
||||
function command::to_namespace() { echo "${1//\//:}"; }
|
||||
|
||||
# Build fully qualified function name
|
||||
# e.g. command::fn "add" "run" -> cmd::add::run
|
||||
# e.g. command::fn "service/wg" "run" -> cmd::service::wg::run
|
||||
function command::fn() {
|
||||
local name namespace
|
||||
namespace=$(command::to_namespace "$1")
|
||||
echo "${_COMMAND_NAMESPACE}::${namespace}::${2}"
|
||||
}
|
||||
|
||||
function command::has_function() { declare -F "$(command::fn "$1" "$2")" >/dev/null 2>&1; }
|
||||
function command::is_auto_load() { declare -F "$(command::fn "$1" on_load)" >/dev/null 2>&1; }
|
||||
function command::exists() {
|
||||
local name="$1"
|
||||
# New-style: has subcommands defined
|
||||
command::has_subcmds "$name" && return 0
|
||||
# Legacy: has cmd::name::run function
|
||||
declare -f "$(command::fn "$name" run)" &>/dev/null
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Runner
|
||||
# ============================================
|
||||
|
||||
function command::run() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
command::_reset_mixin_state
|
||||
|
||||
# Check if this command uses new directory-based routing
|
||||
if command::has_subcmds "$cmd"; then
|
||||
# Route to subcommand
|
||||
command::route "$cmd" "$@"
|
||||
local subcmd="$_ROUTED_SUBCMD"
|
||||
local -a routed_args=("${_ROUTED_ARGS[@]:-}")
|
||||
|
||||
if [[ -z "$subcmd" ]]; then
|
||||
hook::fire "command:help:${cmd}" "$cmd" ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
# log::debug "about to load_subcmd: cmd=$cmd subcmd=$subcmd"
|
||||
# Lazy load subcommand file
|
||||
command::load_subcmd "$cmd" "$subcmd"
|
||||
|
||||
for arg in "$@"; do
|
||||
[[ "$arg" == "--help" || "$arg" == "-h" ]] && {
|
||||
hook::fire "command:help:${cmd}" "$cmd" "$_ROUTED_SUBCMD"
|
||||
return 0
|
||||
}
|
||||
done
|
||||
|
||||
command::run_routed "$cmd" "$subcmd" "${routed_args[@]:-}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Legacy path — existing command::run logic
|
||||
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=("$@")
|
||||
|
||||
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 args=()
|
||||
for _d in "${default_args[@]:-}"; do [[ -n "$_d" ]] && args+=("$_d"); done
|
||||
for _u in "${user_args[@]:-}"; do [[ -n "$_u" ]] && args+=("$_u"); done
|
||||
|
||||
command::_preprocess_flags args
|
||||
|
||||
local fn
|
||||
fn=$(command::fn "$cmd" run)
|
||||
if [[ ${#args[@]} -gt 0 ]]; then
|
||||
core::call_function "$fn" "${args[@]}"
|
||||
else
|
||||
core::call_function "$fn"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function core::call_function() {
|
||||
local fn="$1"
|
||||
shift
|
||||
"$fn" "$@"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Loader
|
||||
# ============================================
|
||||
|
||||
function load_command() {
|
||||
local name="$1"
|
||||
command::loaded "$name" && return 0
|
||||
|
||||
# Check for new directory-based structure first
|
||||
local cmd_dir
|
||||
cmd_dir="$(ctx::commands)/${name}"
|
||||
local cmd_file="${cmd_dir}/${name}.sh"
|
||||
|
||||
if [[ -d "$cmd_dir" && -f "$cmd_file" ]]; then
|
||||
source "$cmd_file"
|
||||
_LOADED_COMMANDS["$name"]=1
|
||||
_CURRENT_LOADING_CMD="$name"
|
||||
core::call_if_exists "cmd::${name}::on_load"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fall back to legacy .command.sh
|
||||
local path
|
||||
path="$(ctx::commands)/${name}.command.sh"
|
||||
if [[ ! -f "$path" ]]; then
|
||||
log::error "Command not found: ${name} (${path})"
|
||||
return 1
|
||||
fi
|
||||
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,35 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Static Context — resolved once at source time
|
||||
# ============================================
|
||||
|
||||
_CTX_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
_CTX_CORE="${_CTX_ROOT}/core"
|
||||
_CTX_FRAMEWORK_CORE="${_CTX_ROOT}/core/framework"
|
||||
_CTX_APP_CORE="${_CTX_ROOT}/core/app"
|
||||
_CTX_MODULES="${_CTX_ROOT}/modules"
|
||||
_CTX_COMMANDS="${_CTX_ROOT}/commands"
|
||||
|
||||
# echo "ctx::root: $_CTX_ROOT"
|
||||
# echo "ctx::modules: $_CTX_MODULES"
|
||||
|
||||
# ============================================
|
||||
# Accessors
|
||||
# ============================================
|
||||
|
||||
function ctx::root() { echo "$_CTX_ROOT"; }
|
||||
function ctx::core() { echo "$_CTX_CORE"; }
|
||||
function ctx::framework() { echo "$_CTX_FRAMEWORK_CORE"; }
|
||||
function ctx::app() { echo "$_CTX_APP_CORE"; }
|
||||
function ctx::modules() { echo "$_CTX_MODULES"; }
|
||||
function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
||||
|
||||
# ============================================
|
||||
# Path Helpers
|
||||
# ============================================
|
||||
|
||||
function ctx::core::path() { local IFS="/"; echo "$_CTX_CORE/$*"; }
|
||||
function ctx::framework::path() { local IFS="/"; echo "$_CTX_FRAMEWORK_CORE/$*"; }
|
||||
function ctx::module::path() { local IFS="/"; echo "$_CTX_MODULES/$*"; }
|
||||
function ctx::command::path() { local IFS="/"; echo "$_CTX_COMMANDS/$*"; }
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/core.sh
|
||||
|
||||
_FRAMEWORK_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
source "${_FRAMEWORK_DIR}/context.sh"
|
||||
source "${_FRAMEWORK_DIR}/module.sh"
|
||||
source "${_FRAMEWORK_DIR}/command.sh"
|
||||
source "${_FRAMEWORK_DIR}/command_mixins.sh"
|
||||
source "${_FRAMEWORK_DIR}/flag.sh"
|
||||
source "${_FRAMEWORK_DIR}/hook.sh"
|
||||
source "${_FRAMEWORK_DIR}/help.sh"
|
||||
source "${_FRAMEWORK_DIR}/mixin.sh"
|
||||
source "${_FRAMEWORK_DIR}/ui.sh"
|
||||
source "${_FRAMEWORK_DIR}/color.sh"
|
||||
source "${_FRAMEWORK_DIR}/fmt.sh"
|
||||
source "${_FRAMEWORK_DIR}/log.sh"
|
||||
source "${_FRAMEWORK_DIR}/utils.sh"
|
||||
source "${_FRAMEWORK_DIR}/test/test.sh"
|
||||
|
||||
command::_load_mixins
|
||||
|
||||
|
|
@ -1,399 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/flag.sh
|
||||
# Optimized: pre-parsed constraints + per-command flag index
|
||||
|
||||
# ── Storage ────────────────────────────────────────────────────────────────
|
||||
|
||||
declare -gA _FLAG_REGISTRY=() # "ctx:--flag" → "type|description"
|
||||
declare -gA _FLAG_INDEX=() # "ctx" → " --flag1 --flag2 ..." (space-separated)
|
||||
|
||||
# Pre-parsed constraints
|
||||
declare -gA _FLAG_C_DEFAULT=()
|
||||
declare -gA _FLAG_C_TYPE=()
|
||||
declare -gA _FLAG_C_MIN=()
|
||||
declare -gA _FLAG_C_MAX=()
|
||||
declare -gA _FLAG_C_CHOICES=()
|
||||
declare -gA _FLAG_C_REQUIRED=()
|
||||
declare -gA _FLAG_C_LABEL=()
|
||||
declare -gA _FLAG_C_SECTION=()
|
||||
|
||||
# Runtime values
|
||||
declare -gA _FLAG_VALUES=()
|
||||
declare -gA _FLAG_ARRAYS=()
|
||||
declare -gA _FLAG_SET=()
|
||||
declare -ga _FLAG_ARGS=()
|
||||
|
||||
# Completion registry
|
||||
declare -gA _FLAG_COMPLETION=()
|
||||
|
||||
# ── Pure bash constraint extractor ────────────────────────────────────────
|
||||
|
||||
function flag::_extract_constraint() {
|
||||
local constraints="$1" lookup="$2"
|
||||
constraints="${constraints#[}"; constraints="${constraints%]}"
|
||||
local rest="$constraints"
|
||||
while [[ -n "$rest" ]]; do
|
||||
local pair="${rest%%,*}"
|
||||
pair="${pair# }"; pair="${pair% }"
|
||||
local k="${pair%%=*}"; local v="${pair#*=}"
|
||||
k="${k% }"; v="${v# }"; v="${v//\"/}"
|
||||
if [[ "$k" == "$lookup" ]]; then
|
||||
printf '%s' "$v"
|
||||
return 0
|
||||
fi
|
||||
[[ "$rest" == *,* ]] && rest="${rest#*,}" || break
|
||||
done
|
||||
}
|
||||
|
||||
function flag::_parse_and_cache() {
|
||||
local key="$1" constraints="$2"
|
||||
[[ -z "$constraints" ]] && return 0
|
||||
# log::debug "parse_and_cache: key=$key"
|
||||
local v
|
||||
v=$(flag::_extract_constraint "$constraints" "default")
|
||||
[[ -n "$v" ]] && _FLAG_C_DEFAULT["$key"]="$v"
|
||||
v=$(flag::_extract_constraint "$constraints" "type")
|
||||
[[ -n "$v" ]] && _FLAG_C_TYPE["$key"]="$v"
|
||||
v=$(flag::_extract_constraint "$constraints" "min")
|
||||
[[ -n "$v" ]] && _FLAG_C_MIN["$key"]="$v"
|
||||
v=$(flag::_extract_constraint "$constraints" "max")
|
||||
[[ -n "$v" ]] && _FLAG_C_MAX["$key"]="$v"
|
||||
v=$(flag::_extract_constraint "$constraints" "choices")
|
||||
# log::debug "parse_and_cache: choices='$v'"
|
||||
[[ -n "$v" ]] && _FLAG_C_CHOICES["$key"]="$v"
|
||||
v=$(flag::_extract_constraint "$constraints" "required")
|
||||
[[ "$v" == "true" ]] && _FLAG_C_REQUIRED["$key"]="true"
|
||||
v=$(flag::_extract_constraint "$constraints" "label")
|
||||
[[ -n "$v" ]] && _FLAG_C_LABEL["$key"]="$v"
|
||||
v=$(flag::_extract_constraint "$constraints" "section")
|
||||
[[ -n "$v" ]] && _FLAG_C_SECTION["$key"]="$v"
|
||||
}
|
||||
|
||||
# ── Constraints ────────────────────────────────────────
|
||||
|
||||
# flag::set_constraint <flag> <key> [value]
|
||||
# Set a constraint on an already-defined flag (Option B syntax)
|
||||
# Examples:
|
||||
# flag::set_constraint --mode choices "split|full"
|
||||
# flag::set_constraint --mode required
|
||||
# flag::set_constraint --mode section Options
|
||||
function flag::set_constraint() {
|
||||
local flag="$1" k="$2" v="${3:-}"
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
local key="${ctx}:${flag}"
|
||||
|
||||
[[ -z "${_FLAG_REGISTRY[$key]+x}" ]] && \
|
||||
log::error "flag::set_constraint: flag not defined: ${flag}" && return 1
|
||||
|
||||
case "$k" in
|
||||
label) _FLAG_C_LABEL["$key"]="$v" ;;
|
||||
default) _FLAG_C_DEFAULT["$key"]="$v" ;;
|
||||
type) _FLAG_C_TYPE["$key"]="$v" ;;
|
||||
min) _FLAG_C_MIN["$key"]="$v" ;;
|
||||
max) _FLAG_C_MAX["$key"]="$v" ;;
|
||||
choices) _FLAG_C_CHOICES["$key"]="$v" ;;
|
||||
section)
|
||||
_FLAG_C_SECTION["$key"]="$v"
|
||||
declare -f help::_assign_flag_from_cache &>/dev/null && \
|
||||
help::_assign_flag_from_cache "$flag" "$v"
|
||||
;;
|
||||
required) _FLAG_C_REQUIRED["$key"]="true" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Context ───────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::_context() { printf '%s' "${_CURRENT_COMMAND:-__global__}"; }
|
||||
function flag::_key() { printf '%s:%s' "${_CURRENT_COMMAND:-__global__}" "$1"; }
|
||||
|
||||
# ── Registration ──────────────────────────────────────────────────────────
|
||||
|
||||
# flag::define <flag> [bool|value] "description" [constraints...]
|
||||
# flag::define <flag[]> "description" [constraints...]
|
||||
#
|
||||
# Constraints (positional, after description):
|
||||
# label:name Display label in help
|
||||
# default:50 Default value
|
||||
# type:int Value type (int, string)
|
||||
# min:1 Minimum value (int)
|
||||
# max:100 Maximum value (int)
|
||||
# choices:a|b|c Allowed values (pipe-separated)
|
||||
# section:Filters Help section
|
||||
# required Mark as required (no value needed)
|
||||
#
|
||||
# Examples:
|
||||
# flag::define --fw bool "Firewall only" section:Filters
|
||||
# flag::define --limit value "Max results" default:50 type:int min:1 section:Output
|
||||
# flag::define --mode value "Tunnel mode" required choices:split|full section:Options
|
||||
# flag::define --svc[] "Exclude service" label:service section:Filters
|
||||
function flag::define() {
|
||||
local raw_flag="$1"
|
||||
local is_array=false
|
||||
|
||||
if [[ "$raw_flag" == *"[]" ]]; then
|
||||
is_array=true
|
||||
raw_flag="${raw_flag%[]}"
|
||||
fi
|
||||
|
||||
local type description
|
||||
local -a constraints=()
|
||||
|
||||
if $is_array; then
|
||||
type="array"
|
||||
description="${2:-}"
|
||||
shift 2
|
||||
constraints=("$@")
|
||||
else
|
||||
type="${2:-bool}"
|
||||
description="${3:-}"
|
||||
shift 3
|
||||
constraints=("$@")
|
||||
fi
|
||||
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
local key="${ctx}:${raw_flag}"
|
||||
|
||||
_FLAG_REGISTRY["$key"]="${type}|${description}"
|
||||
_FLAG_INDEX["$ctx"]+=" ${raw_flag}"
|
||||
|
||||
# Parse constraints
|
||||
flag::_parse_constraints_from_args "$key" "${constraints[@]:-}"
|
||||
|
||||
# Bool default
|
||||
if [[ "$type" == "bool" && -z "${_FLAG_C_DEFAULT[$key]:-}" ]]; then
|
||||
_FLAG_C_DEFAULT["$key"]="false"
|
||||
fi
|
||||
|
||||
# Help section — constraint takes precedence, then active section
|
||||
local section="${_FLAG_C_SECTION[$key]:-${_CURRENT_HELP_SECTION:-}}"
|
||||
if [[ -n "$section" ]]; then
|
||||
_FLAG_C_SECTION["$key"]="$section"
|
||||
declare -f help::_assign_flag_from_cache &>/dev/null && \
|
||||
help::_assign_flag_from_cache "$raw_flag" "$section"
|
||||
fi
|
||||
|
||||
_FLAG_COMPLETION["$raw_flag"]=1
|
||||
}
|
||||
|
||||
function flag::register() { _FLAG_COMPLETION["${1:-}"]=1; }
|
||||
|
||||
# ── Parsing ───────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::parse() {
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
# log::debug "ctx=$ctx index='${_FLAG_INDEX[$ctx]:-EMPTY}'" >&2
|
||||
|
||||
# Reset runtime
|
||||
_FLAG_VALUES=(); _FLAG_ARRAYS=(); _FLAG_SET=(); _FLAG_ARGS=()
|
||||
|
||||
# Check for --help/-h first (fast path)
|
||||
local arg
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
|
||||
local cmd="${_CURRENT_COMMAND%%::*}"
|
||||
declare -f hook::fire &>/dev/null && \
|
||||
hook::fire "command:help:${cmd}" "$cmd" "${_CURRENT_COMMAND##*::}"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Initialize from per-command index (fast — no full registry scan)
|
||||
local flag key type default_val
|
||||
for flag in ${_FLAG_INDEX[$ctx]:-}; do
|
||||
key="${ctx}:${flag}"
|
||||
[[ -z "${_FLAG_REGISTRY[$key]+x}" ]] && continue
|
||||
type="${_FLAG_REGISTRY[$key]%%|*}"
|
||||
default_val="${_FLAG_C_DEFAULT[$key]:-}"
|
||||
case "$type" in
|
||||
bool) _FLAG_VALUES["$flag"]="${default_val:-false}" ;;
|
||||
value) [[ -n "$default_val" ]] && _FLAG_VALUES["$flag"]="$default_val" ;;
|
||||
array) _FLAG_ARRAYS["$flag"]="" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
arg="$1"
|
||||
|
||||
if [[ "$arg" == "--" ]]; then
|
||||
shift; _FLAG_ARGS+=("$@"); break
|
||||
fi
|
||||
|
||||
if [[ "$arg" != --* ]]; then
|
||||
_FLAG_ARGS+=("$arg"); shift; continue
|
||||
fi
|
||||
|
||||
key="${ctx}:${arg}"
|
||||
if [[ -z "${_FLAG_REGISTRY[$key]+x}" ]]; then
|
||||
log::error "Unknown flag: ${arg}"; return 1
|
||||
fi
|
||||
|
||||
type="${_FLAG_REGISTRY[$key]%%|*}"
|
||||
|
||||
case "$type" in
|
||||
bool)
|
||||
_FLAG_VALUES["$arg"]="true"
|
||||
_FLAG_SET["$arg"]="1"
|
||||
shift
|
||||
;;
|
||||
value)
|
||||
if [[ $# -lt 2 || "$2" == --* ]]; then
|
||||
log::error "Flag ${arg} requires a value"; return 1
|
||||
fi
|
||||
local val="$2"
|
||||
local vtype="${_FLAG_C_TYPE[$key]:-}"
|
||||
if [[ "$vtype" == "int" ]]; then
|
||||
if ! [[ "$val" =~ ^[0-9]+$ ]]; then
|
||||
log::error "Flag ${arg} requires an integer, got: ${val}"; return 1
|
||||
fi
|
||||
local min="${_FLAG_C_MIN[$key]:-}" max="${_FLAG_C_MAX[$key]:-}"
|
||||
[[ -n "$min" && "$val" -lt "$min" ]] && \
|
||||
log::error "Flag ${arg} minimum is ${min}, got: ${val}" && return 1
|
||||
[[ -n "$max" && "$val" -gt "$max" ]] && \
|
||||
log::error "Flag ${arg} maximum is ${max}, got: ${val}" && return 1
|
||||
fi
|
||||
local choices="${_FLAG_C_CHOICES[$key]:-}"
|
||||
# log::debug "choices check: flag=$arg val=$val choices='${_FLAG_C_CHOICES[$key]:-EMPTY}'"
|
||||
if [[ -n "$choices" ]]; then
|
||||
local valid=false
|
||||
local choice
|
||||
local saved_ifs="$IFS"
|
||||
IFS=',' read -ra choice_list <<< "$choices"
|
||||
IFS="$saved_ifs"
|
||||
for choice in "${choice_list[@]}"; do
|
||||
[[ "$val" == "$choice" ]] && valid=true && break
|
||||
done
|
||||
if ! $valid; then
|
||||
log::error "Flag ${arg} must be one of: ${choices//|/, }, got: ${val}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_FLAG_VALUES["$arg"]="$val"
|
||||
_FLAG_SET["$arg"]="1"
|
||||
shift 2
|
||||
;;
|
||||
array)
|
||||
if [[ $# -lt 2 || "$2" == --* ]]; then
|
||||
log::error "Flag ${arg} requires a value"; return 1
|
||||
fi
|
||||
if [[ -n "${_FLAG_ARRAYS[$arg]:-}" ]]; then
|
||||
_FLAG_ARRAYS["$arg"]+=$'\n'"$2"
|
||||
else
|
||||
_FLAG_ARRAYS["$arg"]="$2"
|
||||
fi
|
||||
_FLAG_SET["$arg"]="1"
|
||||
shift 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required
|
||||
for flag in ${_FLAG_INDEX[$ctx]:-}; do
|
||||
key="${ctx}:${flag}"
|
||||
[[ -z "${_FLAG_C_REQUIRED[$key]:-}" ]] && continue
|
||||
if [[ -z "${_FLAG_SET[$flag]+x}" && -z "${_FLAG_VALUES[$flag]:-}" ]]; then
|
||||
log::error "Flag ${flag} is required"; return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Validate exclusive groups
|
||||
local cmd="${_CURRENT_COMMAND%%::*}"
|
||||
local groups=""
|
||||
[[ -n "$cmd" ]] && groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
||||
|
||||
if [[ -n "$groups" ]]; then
|
||||
local group
|
||||
while IFS= read -r group; do
|
||||
[[ -z "$group" ]] && continue
|
||||
local -a members=()
|
||||
local OLD_IFS="$IFS"; IFS=','; read -ra members <<< "$group"; IFS="$OLD_IFS"
|
||||
local found_count=0 found_flags="" member
|
||||
for member in "${members[@]}"; do
|
||||
flag::set "$member" && (( found_count++ )) && found_flags+=" $member"
|
||||
done
|
||||
if [[ $found_count -gt 1 ]]; then
|
||||
log::error "Flags${found_flags} are mutually exclusive"; return 1
|
||||
fi
|
||||
done < <(printf '%s' "$groups" | tr '|' '\n')
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# flag::_parse_constraints_from_args <key> <constraint_args...>
|
||||
# Parse variadic constraint args: label:x required choices:a|b section:x
|
||||
function flag::_parse_constraints_from_args() {
|
||||
local key="$1"
|
||||
shift
|
||||
local arg k v
|
||||
|
||||
for arg in "$@"; do
|
||||
# Handle bare keywords (no colon) — e.g. "required"
|
||||
if [[ "$arg" != *:* ]]; then
|
||||
case "$arg" in
|
||||
required) _FLAG_C_REQUIRED["$key"]="true" ;;
|
||||
optional) ;; # default, no-op
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
k="${arg%%:*}"
|
||||
v="${arg#*:}"
|
||||
|
||||
case "$k" in
|
||||
label) _FLAG_C_LABEL["$key"]="$v" ;;
|
||||
default) _FLAG_C_DEFAULT["$key"]="$v" ;;
|
||||
type) _FLAG_C_TYPE["$key"]="$v" ;;
|
||||
min) _FLAG_C_MIN["$key"]="$v" ;;
|
||||
max) _FLAG_C_MAX["$key"]="$v" ;;
|
||||
choices) _FLAG_C_CHOICES["$key"]="$v" ;;
|
||||
section) _FLAG_C_SECTION["$key"]="$v" ;;
|
||||
required) _FLAG_C_REQUIRED["$key"]="$v" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
# ── Accessors ─────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::bool() { [[ "${_FLAG_VALUES[$1]:-false}" == "true" ]]; }
|
||||
function flag::value() { printf '%s' "${_FLAG_VALUES[$1]:-}"; }
|
||||
function flag::array() { printf '%s' "${_FLAG_ARRAYS[$1]:-}"; }
|
||||
function flag::set() { [[ -n "${_FLAG_SET[$1]+x}" ]]; }
|
||||
function flag::args() { printf '%s ' "${_FLAG_ARGS[@]:-}"; }
|
||||
|
||||
function flag::args_array() {
|
||||
local -n _arr_ref="$1"
|
||||
_arr_ref=("${_FLAG_ARGS[@]:-}")
|
||||
}
|
||||
|
||||
# ── Reset ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::_reset_runtime() {
|
||||
_FLAG_VALUES=(); _FLAG_ARRAYS=(); _FLAG_SET=(); _FLAG_ARGS=()
|
||||
}
|
||||
|
||||
function flag::_reset_registry() {
|
||||
local ctx="${1:-$(flag::_context)}"
|
||||
local key
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" == "${ctx}:"* ]] || continue
|
||||
unset "_FLAG_REGISTRY[$key]" "_FLAG_C_DEFAULT[$key]" "_FLAG_C_TYPE[$key]"
|
||||
unset "_FLAG_C_MIN[$key]" "_FLAG_C_MAX[$key]" "_FLAG_C_CHOICES[$key]"
|
||||
unset "_FLAG_C_REQUIRED[$key]" "_FLAG_C_LABEL[$key]" "_FLAG_C_SECTION[$key]"
|
||||
done
|
||||
unset "_FLAG_INDEX[$ctx]"
|
||||
}
|
||||
|
||||
# ── Help bridge ───────────────────────────────────────────────────────────
|
||||
|
||||
function help::_assign_flag_from_cache() {
|
||||
local flag="$1" section="$2"
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
[[ -z "$section" ]] && section="${_CURRENT_HELP_SECTION:-}"
|
||||
[[ -z "$section" ]] && return 0
|
||||
local skey="${ctx}:${section}"
|
||||
if [[ -z "${_HELP_SECTIONS[$skey]+x}" ]]; then
|
||||
_HELP_SECTIONS["$skey"]=$(( _HELP_SECTION_COUNT++ ))
|
||||
fi
|
||||
_HELP_FLAG_SECTION["${ctx}:${flag}"]="$section"
|
||||
}
|
||||
|
|
@ -1,346 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/flag.sh
|
||||
#
|
||||
# Declarative flag registration and parsing framework.
|
||||
#
|
||||
# Registration (in on_load):
|
||||
# flag::define --name value "Filter by peer name"
|
||||
# flag::define --fw bool "Firewall events only"
|
||||
# flag::define --limit value "Max results" [default=50, type=int, min=1]
|
||||
# flag::define --exclude[] "Exclude service" [label="service"]
|
||||
# flag::exclusive --fw --wg
|
||||
#
|
||||
# Parsing (in run):
|
||||
# flag::parse "$@" || return 1
|
||||
#
|
||||
# Accessors:
|
||||
# flag::bool --fw → exits 0/1
|
||||
# flag::value --name → prints value or default
|
||||
# flag::array --exclude → prints newline-separated values
|
||||
# flag::set --name → exits 0 if explicitly passed
|
||||
# flag::args → prints remaining positional args
|
||||
|
||||
# ── Storage ────────────────────────────────────────────────────────────────
|
||||
|
||||
# Per-command flag registry — populated by flag::define during on_load
|
||||
# Key: "cmd::subcmd:--flag"
|
||||
# Value: "type|description|constraints"
|
||||
declare -gA _FLAG_REGISTRY=()
|
||||
|
||||
# Runtime values — populated by flag::parse during run
|
||||
declare -gA _FLAG_VALUES=() # --flag → value (bool: "true"/"false")
|
||||
declare -gA _FLAG_ARRAYS=() # --flag → newline-separated values
|
||||
declare -gA _FLAG_SET=() # --flag → "1" if explicitly passed
|
||||
declare -ga _FLAG_ARGS=() # remaining positional args
|
||||
|
||||
# Defaults — populated at registration time
|
||||
declare -gA _FLAG_DEFAULTS=() # "cmd::subcmd:--flag" → default value
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::_context() {
|
||||
echo "${_CURRENT_COMMAND:-__global__}"
|
||||
}
|
||||
|
||||
function flag::_key() {
|
||||
echo "$(flag::_context):${1}"
|
||||
}
|
||||
|
||||
function flag::_parse_constraints() {
|
||||
# Parse "[key=val, key=val]" constraint string
|
||||
# Output: "key=val\nkey=val"
|
||||
local constraints="${1:-}"
|
||||
constraints="${constraints#[}"
|
||||
constraints="${constraints%]}"
|
||||
echo "$constraints" | tr ',' '\n' | sed 's/^ *//' | sed 's/ *$//'
|
||||
}
|
||||
|
||||
function flag::_constraint_get() {
|
||||
local constraints="$1" key="$2"
|
||||
echo "$constraints" | while IFS='=' read -r k v; do
|
||||
k="${k// /}"
|
||||
if [[ "$k" == "$key" ]]; then
|
||||
echo "${v//\"/}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── Registration ─────────────────────────────────────────────────────────────
|
||||
|
||||
# flag::define --flag [bool|value] "description" [constraints]
|
||||
# flag::define --flag[] "description" [constraints] ← array flag
|
||||
function flag::define() {
|
||||
local raw_flag="$1"
|
||||
local is_array=false
|
||||
|
||||
# Detect array flag syntax: --flag[]
|
||||
if [[ "$raw_flag" == *"[]" ]]; then
|
||||
is_array=true
|
||||
raw_flag="${raw_flag%[]}"
|
||||
fi
|
||||
|
||||
local type description constraints=""
|
||||
|
||||
if $is_array; then
|
||||
type="array"
|
||||
description="${2:-}"
|
||||
constraints="${3:-}"
|
||||
else
|
||||
type="${2:-bool}"
|
||||
description="${3:-}"
|
||||
constraints="${4:-}"
|
||||
fi
|
||||
|
||||
local key
|
||||
key=$(flag::_key "$raw_flag")
|
||||
|
||||
_FLAG_REGISTRY["$key"]="${type}|${description}|${constraints}"
|
||||
|
||||
# Extract and store default
|
||||
if [[ -n "$constraints" ]]; then
|
||||
local parsed_constraints
|
||||
parsed_constraints=$(flag::_parse_constraints "$constraints")
|
||||
local default_val
|
||||
default_val=$(flag::_constraint_get "$parsed_constraints" "default")
|
||||
if [[ -n "$default_val" ]]; then
|
||||
_FLAG_DEFAULTS["$key"]="$default_val"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set bool default to false if not specified
|
||||
if [[ "$type" == "bool" && -z "${_FLAG_DEFAULTS[$key]:-}" ]]; then
|
||||
_FLAG_DEFAULTS["$key"]="false"
|
||||
fi
|
||||
|
||||
# Register for shell completion (backward compat)
|
||||
flag::register "$raw_flag" 2>/dev/null || true
|
||||
|
||||
help::_assign_flag_section "$raw_flag" "$constraints"
|
||||
}
|
||||
|
||||
# flag::register remains for backward compat and completion-only registration
|
||||
# Already defined in command.sh — this is a no-op if already defined
|
||||
function flag::register() {
|
||||
: # handled by command.sh completion registry
|
||||
}
|
||||
|
||||
# ── Parsing ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::parse() {
|
||||
local ctx
|
||||
ctx=$(flag::_context)
|
||||
|
||||
# Reset runtime state
|
||||
_FLAG_VALUES=()
|
||||
_FLAG_ARRAYS=()
|
||||
_FLAG_SET=()
|
||||
_FLAG_ARGS=()
|
||||
|
||||
# Initialize bools to false, values to defaults
|
||||
local key
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
local reg="${_FLAG_REGISTRY[$key]}"
|
||||
local type="${reg%%|*}"
|
||||
local default_val="${_FLAG_DEFAULTS[$key]:-}"
|
||||
|
||||
case "$type" in
|
||||
bool) _FLAG_VALUES["$flag"]="${default_val:-false}" ;;
|
||||
value) [[ -n "$default_val" ]] && _FLAG_VALUES["$flag"]="$default_val" ;;
|
||||
array) _FLAG_ARRAYS["$flag"]="" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
local arg="$1"
|
||||
|
||||
if [[ "$arg" == "--" ]]; then
|
||||
shift
|
||||
_FLAG_ARGS+=("$@")
|
||||
break
|
||||
fi
|
||||
|
||||
if [[ "$arg" != --* ]]; then
|
||||
_FLAG_ARGS+=("$arg")
|
||||
shift
|
||||
continue
|
||||
fi
|
||||
|
||||
local key
|
||||
key=$(flag::_key "$arg")
|
||||
|
||||
if [[ -z "${_FLAG_REGISTRY[$key]+x}" ]]; then
|
||||
log::error "Unknown flag: ${arg}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local reg="${_FLAG_REGISTRY[$key]}"
|
||||
local type="${reg%%|*}"
|
||||
local constraints
|
||||
constraints=$(echo "$reg" | cut -d'|' -f3)
|
||||
|
||||
case "$type" in
|
||||
bool)
|
||||
_FLAG_VALUES["$arg"]="true"
|
||||
_FLAG_SET["$arg"]="1"
|
||||
shift
|
||||
;;
|
||||
value)
|
||||
if [[ $# -lt 2 || "$2" == --* ]]; then
|
||||
log::error "Flag ${arg} requires a value"
|
||||
return 1
|
||||
fi
|
||||
local val="$2"
|
||||
# Type validation
|
||||
if [[ -n "$constraints" ]]; then
|
||||
local parsed
|
||||
parsed=$(flag::_parse_constraints "$constraints")
|
||||
local vtype
|
||||
vtype=$(flag::_constraint_get "$parsed" "type")
|
||||
if [[ "$vtype" == "int" ]]; then
|
||||
if ! [[ "$val" =~ ^[0-9]+$ ]]; then
|
||||
log::error "Flag ${arg} requires an integer, got: ${val}"
|
||||
return 1
|
||||
fi
|
||||
local min max
|
||||
min=$(flag::_constraint_get "$parsed" "min")
|
||||
max=$(flag::_constraint_get "$parsed" "max")
|
||||
[[ -n "$min" && "$val" -lt "$min" ]] && \
|
||||
log::error "Flag ${arg} minimum is ${min}, got: ${val}" && return 1
|
||||
[[ -n "$max" && "$val" -gt "$max" ]] && \
|
||||
log::error "Flag ${arg} maximum is ${max}, got: ${val}" && return 1
|
||||
fi
|
||||
local choices
|
||||
choices=$(flag::_constraint_get "$parsed" "choices")
|
||||
if [[ -n "$choices" ]]; then
|
||||
local valid=false
|
||||
local choice
|
||||
IFS='|' read -ra choice_list <<< "$choices"
|
||||
for choice in "${choice_list[@]}"; do
|
||||
[[ "$val" == "$choice" ]] && valid=true && break
|
||||
done
|
||||
if ! $valid; then
|
||||
log::error "Flag ${arg} must be one of: ${choices//|/, }, got: ${val}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
_FLAG_VALUES["$arg"]="$val"
|
||||
_FLAG_SET["$arg"]="1"
|
||||
shift 2
|
||||
;;
|
||||
array)
|
||||
if [[ $# -lt 2 || "$2" == --* ]]; then
|
||||
log::error "Flag ${arg} requires a value"
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "${_FLAG_ARRAYS[$arg]:-}" ]]; then
|
||||
_FLAG_ARRAYS["$arg"]+=$'\n'"$2"
|
||||
else
|
||||
_FLAG_ARRAYS["$arg"]="$2"
|
||||
fi
|
||||
_FLAG_SET["$arg"]="1"
|
||||
shift 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local groups="${_FLAG_EXCLUSIVE_GROUPS[${_CURRENT_COMMAND%%::*}]:-}"
|
||||
if [[ -n "$groups" ]]; then
|
||||
local group
|
||||
while IFS= read -r group; do
|
||||
[[ -z "$group" ]] && continue
|
||||
local -a members=()
|
||||
IFS=',' read -ra members <<< "$group"
|
||||
local found_count=0 found_flags=""
|
||||
for member in "${members[@]}"; do
|
||||
flag::set "$member" && (( found_count++ )) && found_flags+=" $member"
|
||||
done
|
||||
if [[ $found_count -gt 1 ]]; then
|
||||
log::error "Flags${found_flags} are mutually exclusive"
|
||||
return 1
|
||||
fi
|
||||
done < <(echo "$groups" | tr '|' '\n')
|
||||
fi
|
||||
|
||||
# Validate required flags
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
local reg="${_FLAG_REGISTRY[$key]}"
|
||||
local constraints
|
||||
constraints=$(echo "$reg" | cut -d'|' -f3)
|
||||
if [[ -n "$constraints" ]]; then
|
||||
local parsed
|
||||
parsed=$(flag::_parse_constraints "$constraints")
|
||||
local required
|
||||
required=$(flag::_constraint_get "$parsed" "required")
|
||||
if [[ "$required" == "true" && -z "${_FLAG_SET[$flag]+x}" ]]; then
|
||||
# Check if provided by defaults
|
||||
if [[ -z "${_FLAG_VALUES[$flag]:-}" ]]; then
|
||||
log::error "Flag ${flag} is required"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Accessors ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# flag::bool --flag → exits 0 if true, 1 if false
|
||||
function flag::bool() {
|
||||
[[ "${_FLAG_VALUES[$1]:-false}" == "true" ]]
|
||||
}
|
||||
|
||||
# flag::value --flag → prints value or empty string
|
||||
function flag::value() {
|
||||
echo "${_FLAG_VALUES[$1]:-}"
|
||||
}
|
||||
|
||||
# flag::array --flag → prints newline-separated values
|
||||
function flag::array() {
|
||||
echo "${_FLAG_ARRAYS[$1]:-}"
|
||||
}
|
||||
|
||||
# flag::set --flag → exits 0 if explicitly passed by user
|
||||
function flag::set() {
|
||||
[[ -n "${_FLAG_SET[$1]+x}" ]]
|
||||
}
|
||||
|
||||
# flag::args → prints positional args array
|
||||
function flag::args() {
|
||||
echo "${_FLAG_ARGS[@]:-}"
|
||||
}
|
||||
|
||||
# flag::args_array → populates a nameref array with positional args
|
||||
function flag::args_array() {
|
||||
local -n _arr_ref="$1"
|
||||
_arr_ref=("${_FLAG_ARGS[@]:-}")
|
||||
}
|
||||
|
||||
# ── Reset ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::_reset_runtime() {
|
||||
_FLAG_VALUES=()
|
||||
_FLAG_ARRAYS=()
|
||||
_FLAG_SET=()
|
||||
_FLAG_ARGS=()
|
||||
}
|
||||
|
||||
function flag::_reset_registry() {
|
||||
# Clear registry entries for a specific command context
|
||||
local ctx="${1:-$(flag::_context)}"
|
||||
local key
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" == "${ctx}:"* ]] && unset "_FLAG_REGISTRY[$key]"
|
||||
done
|
||||
for key in "${!_FLAG_DEFAULTS[@]}"; do
|
||||
[[ "$key" == "${ctx}:"* ]] && unset "_FLAG_DEFAULTS[$key]"
|
||||
done
|
||||
}
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/help.sh
|
||||
#
|
||||
# Dynamic help generation from command::define and flag::define metadata.
|
||||
#
|
||||
# Usage in on_load:
|
||||
# help::section "Filters"
|
||||
# help::section "Output"
|
||||
#
|
||||
# Flags assigned to sections via flag::define constraint:
|
||||
# flag::define --fw bool "Firewall only" [section="Filters"]
|
||||
#
|
||||
# Or via active section (set before flag::define calls):
|
||||
# help::section "Filters"
|
||||
# command::mixin time_filter [section="Filters"]
|
||||
# flag::define --fw bool "Firewall only" # inherits "Filters"
|
||||
#
|
||||
# Auto-generate help:
|
||||
# hook::on "command:help" command::help::auto
|
||||
#
|
||||
# Or custom:
|
||||
# hook::on "command:help" cmd::logs::help
|
||||
|
||||
# ── Storage ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# Section registry: "ctx:section_name" → order_index
|
||||
declare -gA _HELP_SECTIONS=()
|
||||
declare -gi _HELP_SECTION_COUNT=0
|
||||
|
||||
# Flag-to-section mapping: "ctx:--flag" → section_name
|
||||
declare -gA _HELP_FLAG_SECTION=()
|
||||
|
||||
# Current active section (set by help::section, used by subsequent flag::define)
|
||||
declare -g _CURRENT_HELP_SECTION=""
|
||||
|
||||
# Command descriptions: "ctx" → description
|
||||
declare -gA _HELP_CMD_DESC=()
|
||||
|
||||
# ── Section registration ──────────────────────────────────────────────────────
|
||||
|
||||
# help::section "Section Name"
|
||||
# Registers a section and sets it as active for subsequent flag::define calls
|
||||
function help::section() {
|
||||
local name="${1:-}"
|
||||
[[ -z "$name" ]] && return 1
|
||||
|
||||
local ctx
|
||||
ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
local key="${ctx}:${name}"
|
||||
|
||||
if [[ -z "${_HELP_SECTIONS[$key]+x}" ]]; then
|
||||
_HELP_SECTIONS["$key"]=$(( _HELP_SECTION_COUNT++ ))
|
||||
fi
|
||||
|
||||
_CURRENT_HELP_SECTION="$name"
|
||||
}
|
||||
|
||||
# ── Auto help generation ──────────────────────────────────────────────────────
|
||||
|
||||
# command::help::auto
|
||||
# Generates help from registered metadata.
|
||||
# Called via: hook::on "command:help" command::help::auto
|
||||
function command::help::auto() {
|
||||
local cmd="${1:-}" subcmd="${2:-}"
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
|
||||
local desc="${_HELP_CMD_DESC[$ctx]:-}"
|
||||
|
||||
# Usage line — build from required flags
|
||||
local usage_parts=()
|
||||
local key
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
local reg="${_FLAG_REGISTRY[$key]}"
|
||||
local type="${reg%%|*}"
|
||||
local constraints
|
||||
constraints=$(echo "$reg" | cut -d'|' -f3)
|
||||
|
||||
if [[ -n "$constraints" ]]; then
|
||||
local required; required="${_FLAG_C_REQUIRED[$key]:-}"
|
||||
local label; label="${_FLAG_C_LABEL[$key]:-value}"
|
||||
|
||||
[[ -z "$label" ]] && label="value"
|
||||
|
||||
if [[ "$required" == "true" ]]; then
|
||||
case "$type" in
|
||||
bool) usage_parts+=("$flag") ;;
|
||||
value) usage_parts+=("${flag} <${label}>*") ;;
|
||||
array) usage_parts+=("${flag} <${label}>*") ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Print usage
|
||||
local cmd_path="${cmd}"
|
||||
local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
|
||||
# Only show subcmd in usage if it's not the default
|
||||
[[ -n "$subcmd" && "$subcmd" != "$default_subcmd" ]] && cmd_path="${cmd} ${subcmd}"
|
||||
printf "\nUsage: wgctl %s" "$cmd_path"
|
||||
for part in "${usage_parts[@]:-}"; do
|
||||
printf " %s" "$part"
|
||||
done
|
||||
|
||||
# Show subcommands if any defined
|
||||
local -a subcmds=()
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && subcmds+=("${key#${cmd}:}")
|
||||
done
|
||||
[[ ${#subcmds[@]} -gt 0 ]] && printf " [command]"
|
||||
printf " [options]\n"
|
||||
|
||||
[[ -n "$desc" ]] && printf "\n%s\n" "$desc"
|
||||
|
||||
# Print subcommands
|
||||
if [[ ${#subcmds[@]} -gt 0 ]]; then
|
||||
printf "\nCommands:\n"
|
||||
for sc in "${subcmds[@]}"; do
|
||||
local sc_key="${cmd}:${sc}"
|
||||
local sc_desc="${_COMMAND_DEFS[$sc_key]:-}"
|
||||
|
||||
local sc_text sc_aliases sc_default
|
||||
sc_text=$(echo "$sc_desc" | cut -d'|' -f1)
|
||||
sc_aliases=$(echo "$sc_desc" | cut -d'|' -f2)
|
||||
sc_default=$(echo "$sc_desc" | cut -d'|' -f3)
|
||||
|
||||
# Clean up empty aliases
|
||||
[[ "$sc_aliases" == "false" || "$sc_aliases" == "true" ]] && sc_aliases=""
|
||||
|
||||
local suffix=""
|
||||
[[ "$sc_default" == "true" ]] && suffix=" (default)"
|
||||
[[ -n "$sc_aliases" ]] && suffix+=" aliases: ${sc_aliases}"
|
||||
|
||||
printf " %-12s %s%s\n" "$sc" "$sc_text" "$suffix"
|
||||
done
|
||||
fi
|
||||
|
||||
# Print flags by section
|
||||
# Collect sections for this context, sorted by order
|
||||
local -a section_names=()
|
||||
local -A section_order=()
|
||||
for key in "${!_HELP_SECTIONS[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local sname="${key#${ctx}:}"
|
||||
section_names+=("$sname")
|
||||
section_order["$sname"]="${_HELP_SECTIONS[$key]}"
|
||||
done
|
||||
|
||||
# Sort sections by registration order
|
||||
local -a sorted_sections=()
|
||||
if [[ ${#section_names[@]} -gt 0 ]]; then
|
||||
while IFS= read -r sname; do
|
||||
sorted_sections+=("$sname")
|
||||
done < <(
|
||||
for sname in "${section_names[@]}"; do
|
||||
echo "${section_order[$sname]} $sname"
|
||||
done | sort -n | cut -d' ' -f2-
|
||||
)
|
||||
fi
|
||||
|
||||
# Flags without a section
|
||||
local -a unsectioned=()
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
[[ -z "${_HELP_FLAG_SECTION[${ctx}:${flag}]:-}" ]] && unsectioned+=("$flag")
|
||||
done
|
||||
|
||||
# Print unsectioned flags first as "Options"
|
||||
if [[ ${#unsectioned[@]} -gt 0 ]]; then
|
||||
printf "\nOptions:\n"
|
||||
for flag in "${unsectioned[@]}"; do
|
||||
help::_print_flag "$flag" "$ctx"
|
||||
done
|
||||
fi
|
||||
|
||||
# Print sectioned flags
|
||||
for sname in "${sorted_sections[@]:-}"; do
|
||||
local printed_header=false
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
local flag_section="${_HELP_FLAG_SECTION[${ctx}:${flag}]:-}"
|
||||
[[ "$flag_section" != "$sname" ]] && continue
|
||||
if ! $printed_header; then
|
||||
printf "\n%s:\n" "$sname"
|
||||
printed_header=true
|
||||
fi
|
||||
help::_print_flag "$flag" "$ctx"
|
||||
done
|
||||
done
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function help::_print_flag() {
|
||||
local flag="$1" ctx="$2"
|
||||
local key="${ctx}:${flag}"
|
||||
local reg="${_FLAG_REGISTRY[$key]:-}"
|
||||
[[ -z "$reg" ]] && return 0
|
||||
|
||||
local type="${reg%%|*}"
|
||||
local desc="${reg#*|}"
|
||||
|
||||
# Use pre-cached constraints instead of parsing
|
||||
local label="${_FLAG_C_LABEL[$key]:-value}"
|
||||
local required="${_FLAG_C_REQUIRED[$key]:-}"
|
||||
local default_val="${_FLAG_C_DEFAULT[$key]:-}"
|
||||
local choices="${_FLAG_C_CHOICES[$key]:-}"
|
||||
|
||||
local flag_display="$flag"
|
||||
case "$type" in
|
||||
value) flag_display="${flag} <${label}>" ;;
|
||||
array) flag_display="${flag} <${label}>" ;;
|
||||
esac
|
||||
|
||||
local suffix=""
|
||||
[[ "$required" == "true" ]] && suffix=" *"
|
||||
[[ -n "$default_val" && "$type" != "bool" ]] && suffix+=" [default: ${default_val}]"
|
||||
[[ -n "$choices" ]] && suffix+=" (${choices//|/|})"
|
||||
|
||||
printf " %-28s %s%s\n" "$flag_display" "$desc" "$suffix"
|
||||
}
|
||||
|
||||
# ── Reset ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function help::_reset() {
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
local key
|
||||
for key in "${!_HELP_SECTIONS[@]}"; do
|
||||
[[ "$key" == "${ctx}:"* ]] && unset "_HELP_SECTIONS[$key]"
|
||||
done
|
||||
for key in "${!_HELP_FLAG_SECTION[@]}"; do
|
||||
[[ "$key" == "${ctx}:"* ]] && unset "_HELP_FLAG_SECTION[$key]"
|
||||
done
|
||||
_CURRENT_HELP_SECTION=""
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/hook.sh
|
||||
#
|
||||
# Simple callback registry — fire named hooks, register handlers.
|
||||
#
|
||||
# Usage:
|
||||
# hook::on "command:help" my_handler_function
|
||||
# hook::off "command:help" my_handler_function
|
||||
# hook::fire "command:help" arg1 arg2
|
||||
#
|
||||
# If no handler registered, hook::fire returns 0 silently.
|
||||
# hook::fire returns the exit code of the handler.
|
||||
|
||||
# Registry: event_name → handler_function
|
||||
declare -gA _HOOK_REGISTRY=()
|
||||
|
||||
# hook::on <event> <handler>
|
||||
# Register a handler for an event. Replaces existing handler.
|
||||
function hook::on() {
|
||||
local event="${1:-}" handler="${2:-}"
|
||||
[[ -z "$event" || -z "$handler" ]] && return 1
|
||||
_HOOK_REGISTRY["$event"]="$handler"
|
||||
}
|
||||
|
||||
# hook::off <event>
|
||||
# Remove handler for an event.
|
||||
function hook::off() {
|
||||
local event="${1:-}"
|
||||
[[ -z "$event" ]] && return 1
|
||||
unset "_HOOK_REGISTRY[$event]"
|
||||
}
|
||||
|
||||
# hook::fire <event> [args...]
|
||||
# Call registered handler with args. Silent no-op if no handler.
|
||||
function hook::fire() {
|
||||
local event="${1:-}"
|
||||
shift || true
|
||||
local handler="${_HOOK_REGISTRY[$event]:-}"
|
||||
[[ -z "$handler" ]] && return 0
|
||||
"$handler" "$@"
|
||||
}
|
||||
|
||||
# hook::has <event>
|
||||
# Returns 0 if a handler is registered for event.
|
||||
function hook::has() {
|
||||
[[ -n "${_HOOK_REGISTRY[$1]:-}" ]]
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
JSON_HELPER="$(ctx::json_helper)"
|
||||
JSON_HELPER="${_CTX_ROOT}/core/json_helper.py"
|
||||
|
||||
function json::get() { python3 "$JSON_HELPER" get "$@" </dev/null; }
|
||||
function json::set() { python3 "$JSON_HELPER" set "$@" </dev/null; }
|
||||
|
|
@ -151,26 +151,6 @@ function internal::get_context_icon() {
|
|||
esac
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Profiler
|
||||
# ============================================
|
||||
|
||||
declare -gi _PROFILE_T0=0
|
||||
|
||||
function log::profile_start() {
|
||||
_PROFILE_T0=$(date +%s%3N)
|
||||
}
|
||||
|
||||
function log::profile() {
|
||||
[[ "${LOG_LEVEL:-2}" -gt 0 ]] && return 0
|
||||
local label="${1:-checkpoint}"
|
||||
local now
|
||||
now=$(date +%s%3N)
|
||||
printf " \033[2m[profile] %s: %dms\033[0m\n" \
|
||||
"$label" "$(( now - _PROFILE_T0 ))" >&2
|
||||
_PROFILE_T0=$now # reset for next checkpoint
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Loggers
|
||||
# ============================================
|
||||
2
wgctl
2
wgctl
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
source "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/core/core.sh"
|
||||
source "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/core.sh"
|
||||
|
||||
LOG_LEVEL=DEBUG
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue