feat: main group display, group::has_peer, config validation, full block cleanup on unblock, ui::empty helper, blocks header count

This commit is contained in:
Nuno Duque Nunes 2026-05-17 22:06:21 +00:00
parent 87f6c770ef
commit 7323bf20f1
14 changed files with 536 additions and 197 deletions

View file

@ -94,7 +94,7 @@ function cmd::block::run() {
# Full block if no specific targets # Full block if no specific targets
if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \ if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \
${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then ${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
if peers::is_blocked "$name" || block::has_file "$name"; then if peers::is_blocked "$name"; then
log::wg_warning "Client is already blocked: ${name}" log::wg_warning "Client is already blocked: ${name}"
return 0 return 0
fi fi

View file

@ -11,6 +11,7 @@ function cmd::group::on_load() {
flag::register --type flag::register --type
flag::register --rule flag::register --rule
flag::register --new-name flag::register --new-name
flag::register --main
flag::register --force flag::register --force
} }
@ -83,6 +84,7 @@ function cmd::group::run() {
rename) cmd::group::rename "$@" ;; rename) cmd::group::rename "$@" ;;
peer) cmd::group::peer "$@" ;; peer) cmd::group::peer "$@" ;;
rm-peers) cmd::group::rm_peers "$@" ;; rm-peers) cmd::group::rm_peers "$@" ;;
set-main) cmd::group::set_main "$@" ;;
block) cmd::group::block "$@" ;; block) cmd::group::block "$@" ;;
unblock) cmd::group::unblock "$@" ;; unblock) cmd::group::unblock "$@" ;;
rule) cmd::group::rule "$@" ;; rule) cmd::group::rule "$@" ;;
@ -343,13 +345,14 @@ function cmd::group::peer() {
} }
function cmd::group::peer_add() { function cmd::group::peer_add() {
local name="" peer="" type="" local name="" peer="" type="" set_main=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;; --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;; --peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;;
--type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;; --type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;;
--main) util::require_flag "--main" "${2:-}" || return 1; set_main=true; shift ;;
--help) cmd::group::help; return ;; --help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;; *) log::error "Unknown flag: $1"; return 1 ;;
esac esac
@ -369,6 +372,11 @@ function cmd::group::peer_add() {
group::add_peer "$name" "$peer" group::add_peer "$name" "$peer"
log::wg_success "Added '${peer}' to group '${name}'" log::wg_success "Added '${peer}' to group '${name}'"
if $set_main; then
peers::set_main_group "$peer_name" "$group_name"
log::wg_success "Set '${group_name}' as main group for ${peer_name}"
fi
} }
function cmd::group::peer_remove() { function cmd::group::peer_remove() {
@ -393,6 +401,34 @@ function cmd::group::peer_remove() {
log::wg_success "Removed '${peer}' from group '${name}'" log::wg_success "Removed '${peer}' from group '${name}'"
} }
function cmd::group::set_main() {
local group_name="" peer_name="" type=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) group_name="$2"; shift 2 ;;
--peer) peer_name="$2"; shift 2 ;;
--type) type="$2"; shift 2 ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$group_name" ]] && log::error "Missing --name" && return 1
[[ -z "$peer_name" ]] && log::error "Missing --peer" && return 1
# Resolve peer name
peer_name=$(peers::resolve_and_require "$peer_name" "$type") || return 1
# Verify peer is in the group
if ! group::has_peer "$group_name" "$peer_name"; then
log::error "Peer '${peer_name}' is not in group '${group_name}'"
log::info "Add them first: wgctl group peer add --name ${group_name} --peer ${peer_name}"
return 1
fi
peers::set_main_group "$peer_name" "$group_name"
log::wg_success "Main group for '${peer_name}' set to '${group_name}'"
}
# ============================================ # ============================================
# Remove peers from WireGuard # Remove peers from WireGuard
# ============================================ # ============================================
@ -444,56 +480,6 @@ function cmd::group::_rm_peer_cb() {
cmd::remove::run --name "$peer_name" --force cmd::remove::run --name "$peer_name" --force
} }
# function cmd::group::rm_peers() {
# local name="" force=false
# while [[ $# -gt 0 ]]; do
# case "$1" in
# --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
# --force) force=true; shift ;;
# --help) cmd::group::help; return ;;
# *) log::error "Unknown flag: $1"; return 1 ;;
# esac
# done
# [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
# group::require_exists "$name" || return 1
# 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; then
# read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm
# case "$confirm" in
# [yY][eE][sS]|[yY]) ;;
# *) log::info "Aborted"; return 0 ;;
# esac
# fi
# local count=0
# for peer_name in "${peers_list[@]}"; do
# [[ -z "$peer_name" ]] && continue
# # Skip if peer no longer exists
# if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
# log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
# continue
# fi
# cmd::remove::run --name "$peer_name" --force
# (( count++ )) || true
# done
# log::wg_success "Removed ${count} peers from WireGuard (group '${name}' definition kept)"
# }
# ============================================ # ============================================
# Block / Unblock # Block / Unblock
# ============================================ # ============================================

View file

@ -36,13 +36,24 @@ Examples:
EOF EOF
} }
INSPECT_WIDTH=48 # total visible width of section lines
INSPECT_LABEL_WIDTH=20
# ============================================ # ============================================
# Private helpers # Private helpers
# ============================================ # ============================================
function cmd::inspect::_section() { function cmd::inspect::_section() {
local title="$1" local title="${1:-}" extra="${2:-0}"
printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title" local width=$(( INSPECT_WIDTH + extra ))
local title_len=${#title}
# Account for "── " (3) + " " (1) before dashes
local dash_count=$(( width - title_len - 4 ))
[[ $dash_count -lt 2 ]] && dash_count=2
local dashes
dashes=$(printf '─%.0s' $(seq 1 $dash_count))
printf "\n \033[0;37m── %s %s\033[0m\n" "$title" "$dashes"
} }
function cmd::inspect::_peer_info() { function cmd::inspect::_peer_info() {
@ -66,7 +77,7 @@ function cmd::inspect::_peer_info() {
block::has_specific_rules "$name" 2>/dev/null && is_restricted="true" block::has_specific_rules "$name" 2>/dev/null && is_restricted="true"
local status last_seen endpoint local status last_seen endpoint
status=$(peers::format_status "$name" "$public_key" \ status=$(peers::format_status_verbose "$name" "$public_key" \
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
last_seen=$(peers::format_last_seen "$name" "$public_key" \ last_seen=$(peers::format_last_seen "$name" "$public_key" \
"$is_blocked" "$last_ts" "" "$handshake_ts") "$is_blocked" "$last_ts" "" "$handshake_ts")
@ -103,17 +114,18 @@ function cmd::inspect::_peer_info() {
fi fi
cmd::inspect::_section "Client" cmd::inspect::_section "Client"
ui::row "Name" "$name" printf "\n"
ui::row "IP" "$ip" ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
ui::row "Type" "$(peers::display_type "$type" "$subtype")" ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
ui::row "Rule" "$rule_display" ui::row "Type" "$(peers::display_type "$type" "$subtype")" "${INSPECT_LABEL_WIDTH}"
ui::row "Status" "$(echo -e "$status")" ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
ui::row "Endpoint" "${endpoint:-}" ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
ui::row "Last seen" "$last_seen" ui::row "Endpoint" "${endpoint:-}" "${INSPECT_LABEL_WIDTH}"
ui::row "AllowedIPs" "$allowed_ips" ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}"
ui::row "Public key" "${public_key:-}" ui::row "AllowedIPs" "$allowed_ips" "${INSPECT_LABEL_WIDTH}"
ui::row "Activity (total)" "$activity_total" ui::row "Public key" "${public_key:-}" "${INSPECT_LABEL_WIDTH}"
ui::row "Activity (current)" "$activity_current" ui::row "Activity (total)" "$activity_total" "${INSPECT_LABEL_WIDTH}"
ui::row "Activity (current)" "$activity_current" "${INSPECT_LABEL_WIDTH}"
return 0 return 0
} }
@ -128,7 +140,8 @@ function cmd::inspect::_rule_info() {
cmd::inspect::_section "Rule: ${rule}" cmd::inspect::_section "Rule: ${rule}"
if rule::render_extends_tree "$rule"; then if rule::render_extends_tree "$rule"; then
printf "\n" # printf "\n"
: # no-op
else else
# No inheritance — flat view # No inheritance — flat view
rule::render_flat "$rule" rule::render_flat "$rule"
@ -140,20 +153,44 @@ function cmd::inspect::_blocks_info() {
local name="${1:-}" local name="${1:-}"
block::has_file "$name" || return 0 block::has_file "$name" || return 0
cmd::inspect::_section "Peer Blocks"
local blocked_direct local blocked_direct
blocked_direct=$(block::is_blocked_direct "$name") blocked_direct=$(block::is_blocked_direct "$name")
[[ "$blocked_direct" == "true" ]] && \
printf " \033[1;31m🚫\033[0m blocked directly\n"
local blocked_groups local blocked_groups
blocked_groups=$(block::get_groups "$name") blocked_groups=$(block::get_groups "$name")
local rules_output
rules_output=$(block::get_rules "$name")
# Skip if truly empty
if [[ "$blocked_direct" != "true" ]] && \
ui::empty "$blocked_groups" && \
ui::empty "$rules_output"; then
block::cleanup "$name" # clean up stale empty file
return 0
fi
# Count rules for header
local rule_count=0
while IFS= read -r line; do
[[ -n "$line" ]] && (( rule_count++ )) || true
done <<< "$rules_output"
# Build header like firewall: Blocks (+N)
local header_counts=""
[[ "$rule_count" -gt 0 ]] && header_counts=" (${rule_count})"
[[ "$blocked_direct" == "true" || -n "$blocked_groups" ]] && \
header_counts="${header_counts} 🚫"
cmd::inspect::_section "Blocks${header_counts}"
printf "\n"
[[ "$blocked_direct" == "true" ]] && \
printf " \033[1;31m🚫\033[0m blocked directly\n"
[[ -n "$blocked_groups" ]] && \ [[ -n "$blocked_groups" ]] && \
printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups" printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups"
block::format_rules "$name" block::format_rules "$name"
return 0 return 0
} }
@ -162,20 +199,22 @@ function cmd::inspect::_group_info() {
local groups=() local groups=()
mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name") mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name")
[[ ${#groups[@]} -eq 0 || -z "${groups[0]:-}" ]] && return 0
ui::section "Groups" ui::empty "${groups[*]}" && return 0
if [[ ${#groups[@]} -eq 0 ]] || [[ -z "${groups[0]:-}" ]]; then local count=${#groups[@]}
printf " —\n" cmd::inspect::_section "Groups (${count})"
return 0 printf "\n"
fi
for g in "${groups[@]}"; do for g in "${groups[@]}"; do
[[ -z "$g" ]] && continue [[ -z "$g" ]] && continue
local count local peer_count
count=$(json::count "$(group::path "$g")" "peers") local main_marker=""
printf " %-20s %s peers\n" "$g" "$count" peer_count=$(json::count "$(group::path "$g")" "peers")
[[ "$g" == "$(peers::get_main_group "$name")" ]] && \
main_marker=" \033[0;33m★\033[0m"
printf " \033[0;37m·\033[0m %-20s \033[0;37m%s peers\033[0m%b\n" \
"$g" "$peer_count" "$main_marker"
done done
return 0 return 0
@ -196,24 +235,15 @@ function cmd::inspect::_firewall_info() {
rules_output+=("$line") rules_output+=("$line")
done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG) done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG)
[[ ${#rules_output[@]} -eq 0 || -z "${rules_output[0]:-}" ]] && return 0 ui::empty "${rules_output[*]}" && return 0
# printf "\n \033[0;37m── Firewall (\033[0;32m+%d\033[0m \033[0;31m-%d\033[0m) \033[0m%s\n" \
# "$accepts" "$drops" "$(printf '─%.0s' {1..28})"
printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \ printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \
"$(color::green "+${accepts}")" \ "$(color::green "+${accepts}")" \
"$(color::red "-${drops}")" \ "$(color::red "-${drops}")" \
"$(printf '─%.0s' {1..28})" "$(printf '\033[0;37m─%.0s' {1..28})"
fw::list_peer_rules "$ip" false fw::list_peer_rules "$ip" false
# if [[ ${#rules_output[@]} -gt 0 ]]; then
# for line in "${rules_output[@]}"; do
# fw::format_rule "$line"
# done
# fi
return 0 return 0
} }

View file

@ -289,6 +289,76 @@ function cmd::list::_iter_confs() {
done done
} }
# 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]:-}"
# # Apply status filters
# 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 peer_group="${peer_group_map[$client_name]:-}"
# [[ "$peer_group" != "$filter_group" ]] && return 0
# fi
# # Format display values
# local status last_seen display_type rule group_display
# status=$(peers::format_status "$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" "${p_subtypes[$client_name]:-}")
# rule="${p_rules[$client_name]:-—}"
# if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
# # Print header on first match
# if [[ "${_list_header_printed:-false}" == "false" ]]; then
# log::section "WireGuard Clients"
# cmd::list::_render_header $has_groups
# _list_header_printed=true
# fi
# # Update rule counts for summary (outer scope array)
# rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
# # Pad status
# local padded_status
# padded_status=$(ui::pad_status "$status" 25)
# # Render row
# if $has_groups; then
# group_display="${peer_group_map[$client_name]:-—}"
# if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
# group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
# fi
# local rule_col_width=12 group_col_width=12
# [[ "$rule" == "—" ]] && rule_col_width=14
# [[ "$group_display" == "—" ]] && group_col_width=14
# printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
# "$client_name" "$ip" "$display_type" "$rule" \
# "$group_display" "$padded_status" "$last_seen"
# else
# local rule_col_width=12
# [[ "$rule" == "—" ]] && rule_col_width=14
# printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \
# "$client_name" "$ip" "$display_type" "$rule" \
# "$padded_status" "$last_seen"
# fi
# }
function cmd::list::_render_row() { function cmd::list::_render_row() {
local client_name="$1" ip="$2" type="$3" local client_name="$1" ip="$2" type="$3"
@ -307,8 +377,8 @@ function cmd::list::_render_row() {
[[ "$is_restricted" == "true" ]]; }; then return 0; fi [[ "$is_restricted" == "true" ]]; }; then return 0; fi
if [[ -n "$filter_group" ]]; then if [[ -n "$filter_group" ]]; then
local peer_group="${peer_group_map[$client_name]:-}" local all_groups="${peer_group_map[$client_name]:-}"
[[ "$peer_group" != "$filter_group" ]] && return 0 [[ "$all_groups" != *"$filter_group"* ]] && return 0
fi fi
# Format display values # Format display values
@ -329,23 +399,26 @@ function cmd::list::_render_row() {
_list_header_printed=true _list_header_printed=true
fi fi
# Update rule counts for summary (outer scope array)
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
# Pad status
local padded_status local padded_status
padded_status=$(ui::pad_status "$status" 25) padded_status=$(ui::pad_status "$status" 25)
# Render row
if $has_groups; then if $has_groups; then
group_display="${peer_group_map[$client_name]:-}" # Use main group for display, fall back to first group, then —
local main_group="${p_main_groups[$client_name]:-}"
if [[ -n "$main_group" ]]; then
group_display="$main_group"
else
group_display="${peer_group_map[$client_name]:-}"
fi
if [[ -n "${peer_group_map[$client_name]:-}" ]]; then if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
fi fi
local rule_col_width=12 group_col_width=12 local rule_col_width=12 group_col_width=12
[[ "$rule" == "—" ]] && rule_col_width=14 [[ "$rule" == "—" ]] && rule_col_width=14
[[ "$group_display" == "—" ]] && group_col_width=14 [[ "$group_display" == "—" ]] && group_col_width=14
printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \ printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
"$client_name" "$ip" "$display_type" "$rule" \ "$client_name" "$ip" "$display_type" "$rule" \
@ -365,14 +438,15 @@ function cmd::list::_render_row() {
function cmd::list::_precompute_all() { function cmd::list::_precompute_all() {
# Peer data # Peer data
declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=() declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=() p_main_groups=()
while IFS="|" read -r name ip rule subtype last_ts last_evt; do while IFS="|" read -r name ip rule subtype last_ts last_evt main_group; do
[[ -z "$name" ]] && continue [[ -z "$name" ]] && continue
p_ips["$name"]="$ip" p_ips["$name"]="$ip"
p_rules["$name"]="${rule:-}" p_rules["$name"]="${rule:-}"
p_subtypes["$name"]="$subtype" p_subtypes["$name"]="$subtype"
p_last_ts["$name"]="$last_ts" p_last_ts["$name"]="$last_ts"
p_last_evt["$name"]="$last_evt" p_last_evt["$name"]="$last_evt"
p_main_groups["$name"]="${main_group:-}"
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)") done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
# WireGuard handshakes + endpoints # WireGuard handshakes + endpoints
@ -399,7 +473,7 @@ function cmd::list::_precompute_all() {
p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "") p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
done done
# Groups # Groups + main group
has_groups=false has_groups=false
declare -gA peer_group_map=() declare -gA peer_group_map=()
local groups_dir local groups_dir
@ -422,6 +496,65 @@ function cmd::list::_precompute_all() {
done < <(json::peer_transfer "$(config::interface)") done < <(json::peer_transfer "$(config::interface)")
} }
# function cmd::list::_precompute_all() {
# # Peer data
# declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=()
# while IFS="|" read -r name ip rule subtype last_ts last_evt; do
# [[ -z "$name" ]] && continue
# p_ips["$name"]="$ip"
# p_rules["$name"]="${rule:-—}"
# p_subtypes["$name"]="$subtype"
# p_last_ts["$name"]="$last_ts"
# p_last_evt["$name"]="$last_evt"
# done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
# # WireGuard handshakes + endpoints
# 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)
# # Block/restricted status
# declare -gA p_blocked=() p_restricted=()
# cmd::list::_precompute_block_status p_blocked p_restricted
# # Public keys
# 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
# # Groups
# 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
# # Transfer/activity data — keyed by pubkey
# declare -gA p_rx=() p_tx=() p_activity=()
# while IFS="|" read -r pubkey rx tx level; do
# [[ -z "$pubkey" ]] && continue
# p_rx["$pubkey"]="$rx"
# p_tx["$pubkey"]="$tx"
# p_activity["$pubkey"]="$level"
# done < <(json::peer_transfer "$(config::interface)")
# }
function cmd::list::_precompute_block_status() { function cmd::list::_precompute_block_status() {
local -n _blocked="$1" local -n _blocked="$1"
local -n _restricted="$2" local -n _restricted="$2"

View file

@ -191,11 +191,10 @@ function cmd::unblock::_unblock_all() {
# Direct unblock overrides everything — clear all block state # Direct unblock overrides everything — clear all block state
block::set_direct "$name" "$client_ip" "false" block::set_direct "$name" "$client_ip" "false"
block::clear_full_block "$name"
# Force full unblock regardless of group blocks
# (direct unblock = admin override)
block::restore_peer "$name" "$client_ip" block::restore_peer "$name" "$client_ip"
block::remove_file "$name" block::cleanup "$name"
local rule local rule
rule=$(peers::get_meta "$name" "rule") rule=$(peers::get_meta "$name" "rule")

View file

@ -2,63 +2,64 @@
JSON_HELPER="${_CTX_ROOT}/core/json_helper.py" JSON_HELPER="${_CTX_ROOT}/core/json_helper.py"
function json::get() { python3 "$JSON_HELPER" get "$@" </dev/null; } function json::get() { python3 "$JSON_HELPER" get "$@" </dev/null; }
function json::set() { python3 "$JSON_HELPER" set "$@" </dev/null; } function json::set() { python3 "$JSON_HELPER" set "$@" </dev/null; }
function json::delete() { python3 "$JSON_HELPER" delete "$@" </dev/null; } function json::delete() { python3 "$JSON_HELPER" delete "$@" </dev/null; }
function json::append() { python3 "$JSON_HELPER" append "$@" </dev/null; } function json::append() { python3 "$JSON_HELPER" append "$@" </dev/null; }
function json::remove() { python3 "$JSON_HELPER" remove "$@" </dev/null; } function json::remove() { python3 "$JSON_HELPER" remove "$@" </dev/null; }
function json::cat() { python3 "$JSON_HELPER" cat "$@" </dev/null; } function json::cat() { python3 "$JSON_HELPER" cat "$@" </dev/null; }
function json::has_key() { python3 "$JSON_HELPER" has_key "$@" </dev/null; } function json::has_key() { python3 "$JSON_HELPER" has_key "$@" </dev/null; }
function json::filter_values() { python3 "$JSON_HELPER" filter_values "$@" </dev/null; } function json::filter_values() { python3 "$JSON_HELPER" filter_values "$@" </dev/null; }
function json::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; } function json::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; }
function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </dev/null; } function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </dev/null; }
function json::fw_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" fw_events "$@" </dev/null; } function json::fw_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" fw_events "$@" </dev/null; }
function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" wg_events "$@" </dev/null; } function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" wg_events "$@" </dev/null; }
function json::format_fw_event() { echo "$1" | python3 "$JSON_HELPER" format_fw_event "$2"; } function json::format_fw_event() { echo "$1" | python3 "$JSON_HELPER" format_fw_event "$2"; }
function json::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; } function json::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; } function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
function json::follow_logs() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" follow_logs "$@"; } function json::follow_logs() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" follow_logs "$@"; }
function json::count() { python3 "$JSON_HELPER" count "$@" </dev/null; } function json::count() { python3 "$JSON_HELPER" count "$@" </dev/null; }
function json::audit_fw_counts() { python3 "$JSON_HELPER" audit_fw_counts "$@" </dev/null; } function json::audit_fw_counts() { python3 "$JSON_HELPER" audit_fw_counts "$@" </dev/null; }
function json::peer_group_map() { python3 "$JSON_HELPER" peer_group_map "$@" </dev/null; } function json::peer_group_map() { python3 "$JSON_HELPER" peer_group_map "$@" </dev/null; }
function json::peer_groups() { python3 "$JSON_HELPER" peer_groups "$@" </dev/null; } function json::peer_groups() { python3 "$JSON_HELPER" peer_groups "$@" </dev/null; }
function json::peer_data() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" peer_data "$@" </dev/null; } function json::peer_data() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" peer_data "$@" </dev/null; }
function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@" </dev/null; } function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@" </dev/null; }
function json::rule_list_data() { python3 "$JSON_HELPER" rule_list_data "$@" </dev/null; } function json::rule_list_data() { python3 "$JSON_HELPER" rule_list_data "$@" </dev/null; }
function json::group_list_data() { python3 "$JSON_HELPER" group_list_data "$@" </dev/null; } function json::group_list_data() { python3 "$JSON_HELPER" group_list_data "$@" </dev/null; }
function json::fmt_datetime() { python3 "$JSON_HELPER" fmt_datetime "$@" </dev/null; } function json::fmt_datetime() { python3 "$JSON_HELPER" fmt_datetime "$@" </dev/null; }
function json::create_rule() { python3 "$JSON_HELPER" create_rule "$@" </dev/null; } function json::create_rule() { python3 "$JSON_HELPER" create_rule "$@" </dev/null; }
function json::cleanup_config() { python3 "$JSON_HELPER" cleanup_config "$@" </dev/null; } function json::cleanup_config() { python3 "$JSON_HELPER" cleanup_config "$@" </dev/null; }
function json::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "$@" </dev/null; } function json::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "$@" </dev/null; }
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; } function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; }
function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; } function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; }
function json::parse_fw_event() { python3 "$JSON_HELPER" parse_fw_event "$@" </dev/null; } function json::parse_fw_event() { python3 "$JSON_HELPER" parse_fw_event "$@" </dev/null; }
function json::remove_events_filtered() { python3 "$JSON_HELPER" remove_events_filtered "$@" </dev/null; } function json::remove_events_filtered() { python3 "$JSON_HELPER" remove_events_filtered "$@" </dev/null; }
function json::rule_resolve() { python3 "$JSON_HELPER" rule_resolve "$@" </dev/null; } function json::rule_resolve() { python3 "$JSON_HELPER" rule_resolve "$@" </dev/null; }
function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_field "$@" </dev/null; } function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_field "$@" </dev/null; }
function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; } function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; }
function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; } function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; }
function json::get_raw() { python3 "$JSON_HELPER" get_raw "$@" </dev/null; } function json::get_raw() { python3 "$JSON_HELPER" get_raw "$@" </dev/null; }
function json::count_resolved() { python3 "$JSON_HELPER" count_resolved "$(ctx::rules)" "$@" </dev/null; } function json::count_resolved() { python3 "$JSON_HELPER" count_resolved "$(ctx::rules)" "$@" </dev/null; }
function json::block_get() { python3 "$JSON_HELPER" block_get "$@" </dev/null; } function json::block_get() { python3 "$JSON_HELPER" block_get "$@" </dev/null; }
function json::block_is_blocked() { python3 "$JSON_HELPER" block_is_blocked "$@" </dev/null; } function json::block_is_blocked() { python3 "$JSON_HELPER" block_is_blocked "$@" </dev/null; }
function json::block_set_direct() { python3 "$JSON_HELPER" block_set_direct "$@" </dev/null; } function json::block_set_direct() { python3 "$JSON_HELPER" block_set_direct "$@" </dev/null; }
function json::block_add_group() { python3 "$JSON_HELPER" block_add_group "$@" </dev/null; } function json::block_add_group() { python3 "$JSON_HELPER" block_add_group "$@" </dev/null; }
function json::block_remove_group() { python3 "$JSON_HELPER" block_remove_group "$@" </dev/null; } function json::block_remove_group() { python3 "$JSON_HELPER" block_remove_group "$@" </dev/null; }
function json::block_add_rule() { python3 "$JSON_HELPER" block_add_rule "$@" </dev/null; } function json::block_add_rule() { python3 "$JSON_HELPER" block_add_rule "$@" </dev/null; }
function json::block_remove_rule() { python3 "$JSON_HELPER" block_remove_rule "$@" </dev/null; } function json::block_remove_rule() { python3 "$JSON_HELPER" block_remove_rule "$@" </dev/null; }
function json::block_get_rules() { python3 "$JSON_HELPER" block_get_rules "$@" </dev/null; } function json::block_get_rules() { python3 "$JSON_HELPER" block_get_rules "$@" </dev/null; }
function json::block_get_groups() { python3 "$JSON_HELPER" block_get_groups "$@" </dev/null; } function json::block_get_groups() { python3 "$JSON_HELPER" block_get_groups "$@" </dev/null; }
function json::block_get_direct() { python3 "$JSON_HELPER" block_get_direct "$@" </dev/null; } function json::block_get_direct() { python3 "$JSON_HELPER" block_get_direct "$@" </dev/null; }
function json::net_list() { python3 "$JSON_HELPER" net_list "$@" </dev/null; } function json::net_list() { python3 "$JSON_HELPER" net_list "$@" </dev/null; }
function json::net_show() { python3 "$JSON_HELPER" net_show "$@" </dev/null; } function json::net_show() { python3 "$JSON_HELPER" net_show "$@" </dev/null; }
function json::net_exists() { python3 "$JSON_HELPER" net_exists "$@" </dev/null; } function json::net_exists() { python3 "$JSON_HELPER" net_exists "$@" </dev/null; }
function json::net_add_service() { python3 "$JSON_HELPER" net_add_service "$@" </dev/null; } function json::net_add_service() { python3 "$JSON_HELPER" net_add_service "$@" </dev/null; }
function json::net_add_port() { python3 "$JSON_HELPER" net_add_port "$@" </dev/null; } function json::net_add_port() { python3 "$JSON_HELPER" net_add_port "$@" </dev/null; }
function json::net_remove() { python3 "$JSON_HELPER" net_remove "$@" </dev/null; } function json::net_remove() { python3 "$JSON_HELPER" net_remove "$@" </dev/null; }
function json::net_resolve() { python3 "$JSON_HELPER" net_resolve "$@" </dev/null; } function json::net_resolve() { python3 "$JSON_HELPER" net_resolve "$@" </dev/null; }
function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; } function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; }
function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; } function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; }
function json::group_has_peer() { python3 "$JSON_HELPER" group_has_peer "$@" </dev/null; }
function json::peer_transfer() { function json::peer_transfer() {
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \ ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \

View file

@ -640,12 +640,13 @@ def peer_data(clients_dir, meta_dir, events_log):
m = meta.get(name, {}) m = meta.get(name, {})
rule = m.get('rule', '') rule = m.get('rule', '')
subtype = m.get('subtype', '') subtype = m.get('subtype', '')
main_group = m.get('main_group', '')
last_event = last_events.get(name, {}) last_event = last_events.get(name, {})
last_ts = last_event.get('timestamp', '') # raw ISO, no formatting last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
last_evt = last_event.get('event', '') # fixed: was last_event last_evt = last_event.get('event', '') # fixed: was last_event
print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}") print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}|{main_group}")
def iso_to_ts(iso_str): def iso_to_ts(iso_str):
"""Convert ISO timestamp to unix timestamp""" """Convert ISO timestamp to unix timestamp"""
@ -1270,25 +1271,18 @@ def block_add_rule(file, peer_ip, rule_type, name="", target="",
sys.exit(1) sys.exit(1)
def block_remove_rule(file, rule_type, target="", port="", proto=""): def block_remove_rule(file, rule_type, target="", port="", proto=""):
"""Remove matching block rule entry""" data = _block_read(file)
try: if not data:
data = _block_read(file) return
if not data: rules = data.get("rules", [])
return filtered = [r for r in rules if not (
rules = data.get("rules", []) r.get("type") == rule_type and
filtered = [] r.get("target", "") == target and
for r in rules: r.get("port", "") == port and
if r.get("type") == rule_type and \ r.get("proto", "") == proto
r.get("target", "") == target and \ )]
r.get("port", "") == port and \ data["rules"] = filtered
r.get("proto", "") == proto: _block_write(file, data)
continue # remove this one
filtered.append(r)
data["rules"] = filtered
_block_write(file, data)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def block_get_rules(file): def block_get_rules(file):
"""Print rules as pipe-separated lines: name|type|target|port|proto""" """Print rules as pipe-separated lines: name|type|target|port|proto"""
@ -1483,6 +1477,15 @@ def block_is_empty(file):
) )
print("true" if empty else "false") print("true" if empty else "false")
def group_has_peer(file, peer_name):
try:
with open(file) as f:
data = json.load(f)
peers = data.get('peers', [])
print('true' if peer_name in peers else 'false')
except Exception:
print('false')
commands = { commands = {
'get': lambda args: get(args[0], args[1]), 'get': lambda args: get(args[0], args[1]),
'set': lambda args: set_key(args[0], args[1], args[2]), 'set': lambda args: set_key(args[0], args[1], args[2]),
@ -1573,6 +1576,7 @@ commands = {
args[3] if len(args) > 3 else '' args[3] if len(args) > 3 else ''
), ),
'block_is_empty': lambda args: block_is_empty(args[0]), 'block_is_empty': lambda args: block_is_empty(args[0]),
'group_has_peer': lambda args: group_has_peer(args[0], args[1]),
} }
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -41,6 +41,30 @@ function ui::pad() {
printf "%b%${pad}s" "$text" "" printf "%b%${pad}s" "$text" ""
} }
function ui::pad_mb() {
local text="$1" width="${2:-20}"
local visible
visible=$(printf "%b" "$text" | sed 's/\x1b\[[0-9;]*m//g')
local vis_len
vis_len=$(python3 -c "import sys; print(len(sys.stdin.read().rstrip('\n')))" \
<<< "$visible")
local pad=$(( width - vis_len ))
[[ $pad -lt 0 ]] && pad=0
printf "%b%${pad}s" "$text" ""
}
function ui::vis_len_multi() {
# Get visible lengths of multiple strings in one Python call
# Returns newline-separated integers
python3 -c "
import sys, re
ansi = re.compile(r'\x1b\[[0-9;]*m')
for s in sys.argv[1:]:
print(len(ansi.sub('', s)))
" "$@"
}
function ui::pad_status() { function ui::pad_status() {
ui::pad "${1:-}" "${2:-25}" ui::pad "${1:-}" "${2:-25}"
} }
@ -65,3 +89,27 @@ function ui::firewall_rule() {
printf "%s\n" "$rule" printf "%s\n" "$rule"
fi fi
} }
# ============================================
# Content Helpers
# ============================================
function ui::has_content() {
# Returns 0 (true) if content exists, 1 if empty
# Works with strings, arrays, or command output
local value="${1:-}"
[[ -n "$value" ]]
}
function ui::skip_if_empty() {
# Usage: ui::skip_if_empty "$var" || return 0
# Or: ui::skip_if_empty "${array[*]}" || return 0
local value="${1:-}"
[[ -z "${value// }" ]] && return 1 || return 0
}
function ui::empty() {
# ui::empty "$var" && return 0
# ui::empty "${array[*]}" && return 0
[[ -z "${1// }" ]]
}

View file

@ -1,10 +1,11 @@
{ {
"phone-fred": "94.63.0.129", "phone-fred": "94.63.0.129",
"phone-helena": "148.69.46.73", "phone-helena": "148.69.46.73",
"phone-nuno": "148.69.51.201", "phone-nuno": "94.63.0.129",
"tablet-nuno": "148.69.202.5", "tablet-nuno": "148.69.202.5",
"guest-zephyr": "5.13.82.5", "guest-zephyr": "86.120.152.74",
"guest-zephyr-test": "94.63.0.129", "guest-zephyr-test": "94.63.0.129",
"desktop-roboclean": "46.189.215.231", "desktop-roboclean": "46.189.215.231",
"laptop-nuno": "94.63.0.129" "laptop-nuno": "94.63.0.129",
"phone-luis": "176.223.61.15"
} }

View file

@ -106,6 +106,14 @@ function block::rename() {
[[ -f "$old_file" ]] && mv "$old_file" "$new_file" [[ -f "$old_file" ]] && mv "$old_file" "$new_file"
} }
function block::clear_full_block() {
local name="${1:?}"
local file
file=$(block::file "$name")
[[ ! -f "$file" ]] && return 0
json::block_remove_rule "$file" "full"
}
# ── High level operations ────────────────── # ── High level operations ──────────────────
function block::apply_full() { function block::apply_full() {

View file

@ -41,6 +41,69 @@ function config::_init_defaults() {
_WG_TUNNEL_FULL="0.0.0.0/0, ::/0" _WG_TUNNEL_FULL="0.0.0.0/0, ::/0"
} }
# ============================================
# Validation
# ============================================
function config::validate() {
local errors=()
# Required fields
local endpoint
endpoint=$(config::endpoint)
if [[ -z "$endpoint" ]]; then
errors+=("WG_ENDPOINT is not set — required for client config generation")
elif [[ "$endpoint" != *:* ]]; then
errors+=("WG_ENDPOINT must include port (e.g. wg.example.com:51820)")
fi
local port
port=$(config::port)
if [[ -z "$port" ]]; then
errors+=("WG_LISTEN_PORT is not set")
elif ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
errors+=("WG_LISTEN_PORT must be a valid port number (1-65535)")
fi
local dns
dns=$(config::dns)
if [[ -z "$dns" ]]; then
errors+=("WG_DNS is not set — required for client configs")
elif ! ip::is_valid "$dns"; then
errors+=("WG_DNS must be a valid IP address")
fi
local subnet
subnet=$(config::subnet)
if [[ -z "$subnet" ]]; then
errors+=("WG_SUBNET is not set — required for IP allocation")
fi
local interface
interface=$(config::interface)
if [[ -z "$interface" ]]; then
errors+=("WG_INTERFACE is not set, defaulting to wg0")
fi
# Warn-only fields
local lan
lan=$(config::lan)
if [[ -z "$lan" ]]; then
log::wg_warning "WG_LAN is not set — some rule features may not work correctly"
fi
if [[ ${#errors[@]} -gt 0 ]]; then
log::error "wgctl configuration errors:"
for err in "${errors[@]}"; do
printf " ✗ %s\n" "$err" >&2
done
printf "\n Edit /etc/wireguard/.wgctl/wgctl.conf to fix these issues.\n\n" >&2
return 1
fi
return 0
}
# ============================================ # ============================================
# Load overrides from .wgctl/wgctl.conf # Load overrides from .wgctl/wgctl.conf
# ============================================ # ============================================
@ -129,10 +192,7 @@ function config::activity_total_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES";
function config::activity_current_low() { echo "$_ACTIVITY_TOTAL_LOW_BYTES"; } function config::activity_current_low() { echo "$_ACTIVITY_TOTAL_LOW_BYTES"; }
function config::activity_current_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; } function config::activity_current_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; }
function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; } function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; }
function config::server_public_key() { cat "$_WG_SERVER_PUBLIC_KEY_FILE"; }
function config::server_public_key() {
cat "$_WG_SERVER_PUBLIC_KEY_FILE"
}
function config::device_types() { function config::device_types() {
local types local types

View file

@ -84,3 +84,14 @@ function group::_peer_exists_check() {
local peer_name="${1:-}" local peer_name="${1:-}"
peers::require_exists "$peer_name" > /dev/null 2>&1 peers::require_exists "$peer_name" > /dev/null 2>&1
} }
function group::has_peer() {
local group_name="${1:?}" peer_name="${2:?}"
local group_file
group_file="$(group::path "$group_name")"
[[ ! -f "$group_file" ]] && return 1
local result
result=$(json::group_has_peer "$group_file" "$peer_name")
[[ "$result" == "true" ]]
}

View file

@ -286,13 +286,15 @@ function peers::is_offline() {
peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || return 0 peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || return 0
} }
# function peers::is_offline() { function peers::get_main_group() {
# local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}" local name="${1:?}"
# if peers::is_online "$name" "$handshake_ts" "$last_ts"; then peers::get_meta "$name" "main_group"
# return 1 }
# fi
# return 0 function peers::set_main_group() {
# } local name="${1:?}" group="${2:?}"
peers::set_meta "$name" "main_group" "$group"
}
# ============================================ # ============================================
# Name + Type Parsing # Name + Type Parsing
@ -352,6 +354,22 @@ function peers::format_last_seen() {
esac esac
} }
# function peers::format_status() {
# local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
# local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
# local state
# state=$(peers::connection_state "$is_blocked" "$is_restricted" \
# "$handshake_ts" "$last_ts")
# local conn_str modifier color
# IFS="|" read -r conn_str modifier color <<< "$state"
# local display="$conn_str"
# [[ -n "$modifier" ]] && display="${conn_str} (${modifier})"
# echo -e "${color}${display}\033[0m"
# }
function peers::format_status() { function peers::format_status() {
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}" local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}" local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
@ -363,9 +381,47 @@ function peers::format_status() {
local conn_str modifier color local conn_str modifier color
IFS="|" read -r conn_str modifier color <<< "$state" IFS="|" read -r conn_str modifier color <<< "$state"
local display="$conn_str" # Color based on state — modifier overrides base connection color
[[ -n "$modifier" ]] && display="${conn_str} (${modifier})" if [[ "$is_blocked" == "true" ]]; then
echo -e "${color}${display}\033[0m" color="\033[1;31m" # red — blocked
elif [[ "$is_restricted" == "true" ]]; then
color="\033[1;33m" # yellow — restricted
elif [[ "$conn_str" == "online" ]]; then
color="\033[1;32m" # green — online
else
color="\033[0;37m" # gray — offline
fi
local conn_str_padded
conn_str_padded=$(printf "%-7s" "$conn_str")
echo -e "${color}${conn_str_padded}\033[0m"
}
# Inspect — verbose, color + descriptive text
function peers::format_status_verbose() {
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
local conn_str
local state
state=$(peers::connection_state "$is_blocked" "$is_restricted" \
"$handshake_ts" "$last_ts")
IFS="|" read -r conn_str _ _ <<< "$state"
local color suffix=""
if [[ "$is_blocked" == "true" ]]; then
color="\033[1;31m"
suffix=" (blocked)"
elif [[ "$is_restricted" == "true" ]]; then
color="\033[1;33m"
suffix=" (restricted)"
elif [[ "$conn_str" == "online" ]]; then
color="\033[1;32m"
else
color="\033[0;37m"
fi
echo -e "${color}${conn_str}${suffix}\033[0m"
} }
function peers::display_type() { function peers::display_type() {

2
wgctl
View file

@ -70,6 +70,8 @@ function wgctl::dispatch() {
case "$cmd" in case "$cmd" in
help) wgctl::help; return ;; help) wgctl::help; return ;;
shell) : ;;
*) config::validate || exit 1 ;;
esac esac
# If alias resolved to service, pass original cmd as subcommand # If alias resolved to service, pass original cmd as subcommand