feat: identity, subnet, policy systems + tableless layouts
This commit is contained in:
parent
92d829e184
commit
4b2f2a846a
20 changed files with 1523 additions and 493 deletions
|
|
@ -273,25 +273,31 @@ function cmd::add::_apply_identity_rule() {
|
||||||
|
|
||||||
[[ -z "$identity_name" ]] && return 0
|
[[ -z "$identity_name" ]] && return 0
|
||||||
|
|
||||||
local identity_rule
|
local rules
|
||||||
identity_rule=$(identity::rule "$identity_name") || true
|
rules=$(identity::rules "$identity_name")
|
||||||
|
|
||||||
[[ -z "$identity_rule" ]] && {
|
if [[ -z "$rules" ]]; then
|
||||||
# No identity rule — warn if no peer rule either
|
# No identity rules — warn if no peer rule either
|
||||||
if [[ -z "$peer_rule" ]]; then
|
if [[ -z "$peer_rule" ]]; then
|
||||||
policy::warn_no_rule "$full_name"
|
policy::warn_no_rule "$full_name"
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
}
|
fi
|
||||||
|
|
||||||
# Apply identity rule
|
# Apply all identity rules
|
||||||
rule::exists "$identity_rule" && rule::apply "$identity_rule" "$ip" "$full_name" || true
|
rule::_apply_identity_rule "$full_name" "$ip"
|
||||||
|
|
||||||
# Warn based on strict_rule
|
# Warn based on strict_rule
|
||||||
if policy::strict_rule "$effective_policy"; then
|
local strict
|
||||||
policy::warn_strict_rule "$identity_name" "$effective_policy" "$identity_rule"
|
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
||||||
elif [[ -n "$peer_rule" && "$peer_rule" != "$identity_rule" ]]; then
|
if [[ "$strict" == "true" ]]; then
|
||||||
policy::warn_additive_rule "$identity_name" "$identity_rule" "$peer_rule"
|
local rule_list
|
||||||
|
rule_list=$(echo "$rules" | tr '\n' ',' | sed 's/,$//')
|
||||||
|
policy::warn_strict_rule "$identity_name" "$effective_policy" "$rule_list"
|
||||||
|
elif [[ -n "$peer_rule" ]]; then
|
||||||
|
local rule_list
|
||||||
|
rule_list=$(echo "$rules" | tr '\n' ',' | sed 's/,$//')
|
||||||
|
policy::warn_additive_rule "$identity_name" "$rule_list" "$peer_rule"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,16 +101,19 @@ function cmd::identity::run() {
|
||||||
function cmd::identity::_list() {
|
function cmd::identity::_list() {
|
||||||
local data
|
local data
|
||||||
data=$(identity::list_data)
|
data=$(identity::list_data)
|
||||||
|
|
||||||
if [[ -z "$data" ]]; then
|
if [[ -z "$data" ]]; then
|
||||||
log::info "No identities found. Run 'wgctl identity migrate' to create from existing peers."
|
log::info "No identities found. Run 'wgctl identity migrate' to create from existing peers."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ui::identity::header
|
echo ""
|
||||||
while IFS='|' read -r name peer_count types; do
|
while IFS='|' read -r name peer_count types rules policy; do
|
||||||
ui::identity::row "$name" "$peer_count" "$types"
|
local rules_display
|
||||||
|
rules_display=$(echo "$rules" | sed 's/,/, /g')
|
||||||
|
ui::identity::list_row_compact "$name" "$peer_count" "$rules_display" "$policy"
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::identity::_show() {
|
function cmd::identity::_show() {
|
||||||
|
|
@ -122,50 +125,73 @@ function cmd::identity::_show() {
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||||
identity::require_exists "$name" || return 1
|
identity::require_exists "$name" || return 1
|
||||||
|
|
||||||
local data peer_count="0"
|
# Gather identity-level metadata
|
||||||
|
local policy strict auto rules_list peer_count
|
||||||
|
policy=$(identity::policy "$name")
|
||||||
|
strict=$(identity::rule_flags "$name" "strict_rule")
|
||||||
|
auto=$(identity::rule_flags "$name" "auto_apply")
|
||||||
|
rules_list=$(identity::rules "$name" | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g')
|
||||||
|
|
||||||
|
local data
|
||||||
data=$(identity::show_data "$name")
|
data=$(identity::show_data "$name")
|
||||||
|
peer_count=$(echo "$data" | grep '^peer_count|' | cut -d'|' -f2)
|
||||||
|
|
||||||
|
# Precompute handshakes once for all peers in this identity
|
||||||
|
declare -A _id_handshakes=()
|
||||||
|
while IFS=$'\t' read -r pk ts; do
|
||||||
|
[[ -n "$pk" ]] && _id_handshakes["$pk"]="$ts"
|
||||||
|
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
echo ""
|
||||||
|
ui::row "Identity" "$name"
|
||||||
|
ui::row "Policy" "$policy"
|
||||||
|
ui::row "Rules" "${rules_list:-—}"
|
||||||
|
ui::row "Strict rule" "$(ui::bool "$strict")"
|
||||||
|
ui::row "Auto apply" "$(ui::bool "$auto")"
|
||||||
|
ui::row "Peers" "$peer_count"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Device list
|
||||||
while IFS='|' read -r key val type_val index_val; do
|
while IFS='|' read -r key val type_val index_val; do
|
||||||
case "$key" in
|
case "$key" in
|
||||||
name)
|
name|peer_count) ;;
|
||||||
peer_count=$(echo "$data" | grep '^peer_count|' | cut -d'|' -f2)
|
|
||||||
ui::identity::detail_name "$val" "$peer_count"
|
|
||||||
;;
|
|
||||||
peer_count) ;; # consumed above
|
|
||||||
device)
|
device)
|
||||||
local status=""
|
local status=""
|
||||||
status=$(cmd::identity::_device_status "$val")
|
status=$(cmd::identity::_device_status "$val" _id_handshakes)
|
||||||
ui::identity::device_row "$val" "$type_val" "$index_val" "$status"
|
ui::identity::device_row "$val" "$type_val" "$index_val" "$status"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function cmd::identity::_device_status() {
|
function cmd::identity::_device_status() {
|
||||||
local peer_name="${1:-}"
|
local peer_name="${1:-}"
|
||||||
|
local -n _handshakes="${2:-__empty_map}"
|
||||||
|
|
||||||
local peer_ip
|
local peer_ip
|
||||||
peer_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || return 0
|
peer_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || return 0
|
||||||
[[ -z "$peer_ip" ]] && return 0
|
[[ -z "$peer_ip" ]] && return 0
|
||||||
|
|
||||||
local is_blocked is_restricted pubkey handshake_ts
|
local is_blocked is_restricted pubkey handshake_ts
|
||||||
is_blocked=$(peers::is_blocked "$peer_name")
|
peers::is_blocked "$peer_name" && is_blocked="true" || is_blocked="false"
|
||||||
is_restricted=$(peers::is_restricted "$peer_name")
|
peers::is_restricted "$peer_name" && is_restricted="true" || is_restricted="false"
|
||||||
pubkey=$(cat "$(ctx::clients)/${peer_name}.pub" 2>/dev/null) || pubkey=""
|
|
||||||
handshake_ts=$(wg show wg0 latest-handshakes 2>/dev/null \
|
pubkey="$(keys::public "$peer_name")"
|
||||||
| awk -v pk="$pubkey" '$1==pk{print $2}') || handshake_ts=0
|
handshake_ts="${_handshakes[$pubkey]:-0}"
|
||||||
|
|
||||||
local last_ts last_evt
|
local last_ts
|
||||||
last_ts=$(peers::get_meta "$peer_name" "last_ts" 2>/dev/null) || last_ts=""
|
last_ts=$(peers::get_meta "$peer_name" "last_ts" 2>/dev/null) || last_ts=""
|
||||||
last_evt=$(peers::get_meta "$peer_name" "last_evt" 2>/dev/null) || last_evt=""
|
|
||||||
|
|
||||||
local status
|
local status
|
||||||
status=$(peers::format_status \
|
status=$(peers::format_status_verbose \
|
||||||
"$peer_name" "$pubkey" "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
"$peer_name" "$pubkey" "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
echo " — ${status}"
|
echo " — ${status}"
|
||||||
}
|
}
|
||||||
|
|
@ -336,12 +362,19 @@ function cmd::identity::_rule_assign() {
|
||||||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||||
[[ -z "$rule" ]] && { log::error "Missing required flag: --rule"; return 1; }
|
[[ -z "$rule" ]] && { log::error "Missing required flag: --rule"; return 1; }
|
||||||
identity::require_exists "$name" || return 1
|
identity::require_exists "$name" || return 1
|
||||||
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
|
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
|
||||||
|
|
||||||
|
local exit_code
|
||||||
|
identity::add_rule "$name" "$rule" || exit_code=$?
|
||||||
|
|
||||||
|
if [[ $exit_code -eq 2 ]]; then
|
||||||
|
log::warn "Rule '${rule}' is already assigned to identity '${name}'"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
identity::set_rule "$name" "$rule"
|
|
||||||
log::ok "Rule '${rule}' assigned to identity '${name}'"
|
log::ok "Rule '${rule}' assigned to identity '${name}'"
|
||||||
|
|
||||||
# Reapply rules for all peers if auto_apply is set
|
# Reapply rules if auto_apply
|
||||||
local auto
|
local auto
|
||||||
auto=$(identity::rule_flags "$name" "auto_apply")
|
auto=$(identity::rule_flags "$name" "auto_apply")
|
||||||
if [[ "$auto" != "false" ]]; then
|
if [[ "$auto" != "false" ]]; then
|
||||||
|
|
@ -350,17 +383,19 @@ function cmd::identity::_rule_assign() {
|
||||||
log::ok "Rules reapplied"
|
log::ok "Rules reapplied"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Warn about strict_rule impact
|
# Warn about strict_rule
|
||||||
if policy::strict_rule "$(identity::policy "$name")"; then
|
if policy::strict_rule "$(identity::policy "$name")"; then
|
||||||
log::warn "Strict rule is enabled for identity '${name}' — peer rules will not be additive"
|
log::warn "Strict rule is enabled for identity '${name}' — peer rules will not be additive"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::identity::_rule_unassign() {
|
function cmd::identity::_rule_unassign() {
|
||||||
local name=""
|
local name="" rule="" all=false
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
|
--rule) rule="$2"; shift 2 ;;
|
||||||
|
--all) all=true; shift ;;
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
@ -368,16 +403,46 @@ function cmd::identity::_rule_unassign() {
|
||||||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||||
identity::require_exists "$name" || return 1
|
identity::require_exists "$name" || return 1
|
||||||
|
|
||||||
local current_rule
|
if $all; then
|
||||||
current_rule=$(identity::rule "$name")
|
local rules
|
||||||
if [[ -z "$current_rule" ]]; then
|
rules=$(identity::rules "$name")
|
||||||
log::warn "Identity '${name}' has no rule assigned"
|
if [[ -z "$rules" ]]; then
|
||||||
|
log::warn "Identity '${name}' has no rules assigned"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
identity::clear_rules "$name"
|
||||||
|
log::ok "All rules removed from identity '${name}'"
|
||||||
|
cmd::identity::_reapply_after_unassign "$name"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
identity::clear_rule "$name"
|
[[ -z "$rule" ]] && {
|
||||||
log::ok "Rule removed from identity '${name}'"
|
log::error "Missing required flag: --rule (or use --all to remove all)"
|
||||||
log::info "Note: existing fw rules from '${current_rule}' are not automatically removed — run 'wgctl rule reapply' if needed"
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
identity::remove_rule "$name" "$rule"
|
||||||
|
local exit_code=$?
|
||||||
|
if [[ $exit_code -ne 0 ]]; then
|
||||||
|
log::error "Rule '${rule}' is not assigned to identity '${name}'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log::ok "Rule '${rule}' removed from identity '${name}'"
|
||||||
|
cmd::identity::_reapply_after_unassign "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::identity::_reapply_after_unassign() {
|
||||||
|
local name="${1:-}"
|
||||||
|
local auto
|
||||||
|
auto=$(identity::rule_flags "$name" "auto_apply")
|
||||||
|
if [[ "$auto" != "false" ]]; then
|
||||||
|
log::info "Reapplying rules for all peers in identity '${name}'..."
|
||||||
|
identity::reapply_rules "$name"
|
||||||
|
log::ok "Rules reapplied"
|
||||||
|
else
|
||||||
|
log::info "Note: auto_apply is disabled — run 'wgctl audit --fix' to update fw rules"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::identity::_rule_show() {
|
function cmd::identity::_rule_show() {
|
||||||
|
|
@ -392,20 +457,28 @@ function cmd::identity::_rule_show() {
|
||||||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||||
identity::require_exists "$name" || return 1
|
identity::require_exists "$name" || return 1
|
||||||
|
|
||||||
local rule policy
|
local rules policy strict auto
|
||||||
rule=$(identity::rule "$name")
|
rules=$(identity::rules "$name")
|
||||||
policy=$(identity::policy "$name")
|
policy=$(identity::policy "$name")
|
||||||
|
|
||||||
local strict auto
|
|
||||||
strict=$(identity::rule_flags "$name" "strict_rule")
|
strict=$(identity::rule_flags "$name" "strict_rule")
|
||||||
auto=$(identity::rule_flags "$name" "auto_apply")
|
auto=$(identity::rule_flags "$name" "auto_apply")
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
ui::row "Identity" "$name"
|
ui::row "Identity" "$name"
|
||||||
ui::row "Rule" "${rule:-—}"
|
ui::row "Policy" "$policy"
|
||||||
ui::row "Policy" "$policy"
|
ui::row "Strict rule" "$(ui::bool "$strict")"
|
||||||
ui::row "Strict rule" "$( [[ "$strict" == "true" ]] && echo "yes" || echo "no" )"
|
ui::row "Auto apply" "$(ui::bool "$auto")"
|
||||||
ui::row "Auto apply" "$( [[ "$auto" != "false" ]] && echo "yes" || echo "no" )"
|
echo ""
|
||||||
|
|
||||||
|
if [[ -z "$rules" ]]; then
|
||||||
|
ui::row "Rules" "— none assigned"
|
||||||
|
else
|
||||||
|
printf " %-20s\n" "Rules:"
|
||||||
|
while IFS= read -r rule_name; do
|
||||||
|
[[ -z "$rule_name" ]] && continue
|
||||||
|
printf " · %s\n" "$rule_name"
|
||||||
|
done <<< "$rules"
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,9 @@ function cmd::inspect::_peer_info() {
|
||||||
local activity_current
|
local activity_current
|
||||||
activity_current=$(peers::format_activity_current "$public_key")
|
activity_current=$(peers::format_activity_current "$public_key")
|
||||||
|
|
||||||
|
local rule_file=""
|
||||||
local rule_extends=""
|
local rule_extends=""
|
||||||
if [[ -n "$rule" ]]; then
|
if [[ -n "$rule" ]]; then
|
||||||
local rule_file
|
|
||||||
rule_file="$(rule::path "$rule" 2>/dev/null)" || true
|
rule_file="$(rule::path "$rule" 2>/dev/null)" || true
|
||||||
if [[ -n "$rule_file" ]]; then
|
if [[ -n "$rule_file" ]]; then
|
||||||
local ext=()
|
local ext=()
|
||||||
|
|
@ -115,7 +115,7 @@ function cmd::inspect::_peer_info() {
|
||||||
ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
|
ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Type" "$(peers::display_type "$type")" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Type" "$(peers::display_type "$type")" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}"
|
||||||
|
|
@ -127,22 +127,81 @@ function cmd::inspect::_peer_info() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# function cmd::inspect::_rule_info() {
|
||||||
|
# local name="${1:-}"
|
||||||
|
# local rule
|
||||||
|
# rule=$(peers::get_meta "$name" "rule")
|
||||||
|
# [[ -z "$rule" ]] && return 0
|
||||||
|
# rule::exists "$rule" || return 0
|
||||||
|
|
||||||
|
# cmd::inspect::_section "Rule: ${rule}"
|
||||||
|
|
||||||
|
# if ui::rule::tree "$rule"; then
|
||||||
|
# # printf "\n"
|
||||||
|
# : # no-op
|
||||||
|
# else
|
||||||
|
# # No inheritance — flat view
|
||||||
|
# rule::render_flat "$rule"
|
||||||
|
# fi
|
||||||
|
# return 0
|
||||||
|
# }
|
||||||
|
|
||||||
|
function cmd::inspect::_rule_separator() {
|
||||||
|
local line_width=20
|
||||||
|
local total=$INSPECT_WIDTH
|
||||||
|
local pad=$(( (total - line_width) / 2 ))
|
||||||
|
printf "\n%*s\033[2m%s\033[0m\n\n" "$pad" "" "$(printf '─%.0s' $(seq 1 $line_width))"
|
||||||
|
}
|
||||||
|
|
||||||
function cmd::inspect::_rule_info() {
|
function cmd::inspect::_rule_info() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
local rule
|
local rule
|
||||||
rule=$(peers::get_meta "$name" "rule")
|
rule=$(peers::get_meta "$name" "rule")
|
||||||
[[ -z "$rule" ]] && return 0
|
|
||||||
rule::exists "$rule" || return 0
|
local identity_name identity_rules strict
|
||||||
|
identity_name=$(identity::get_name "$name")
|
||||||
cmd::inspect::_section "Rule: ${rule}"
|
if [[ -n "$identity_name" ]]; then
|
||||||
|
identity_rules=$(identity::rules "$identity_name")
|
||||||
if rule::render_extends_tree "$rule"; then
|
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
||||||
# printf "\n"
|
|
||||||
: # no-op
|
|
||||||
else
|
|
||||||
# No inheritance — flat view
|
|
||||||
rule::render_flat "$rule"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Skip section entirely if nothing to show
|
||||||
|
[[ -z "$rule" && -z "$identity_rules" ]] && return 0
|
||||||
|
|
||||||
|
# Build section header
|
||||||
|
local header="Rules"
|
||||||
|
[[ -n "$rule" ]] && header="${header}: ${rule}"
|
||||||
|
[[ -n "$identity_name" && -n "$identity_rules" ]] && \
|
||||||
|
header="${header} · identity:${identity_name}"
|
||||||
|
|
||||||
|
cmd::inspect::_section "$header"
|
||||||
|
|
||||||
|
# Identity block first
|
||||||
|
if [[ -n "$identity_name" && -n "$identity_rules" ]]; then
|
||||||
|
ui::rule::identity_block "$identity_name" "$strict"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Peer rule block — only if set and not suppressed
|
||||||
|
if [[ -n "$rule" ]]; then
|
||||||
|
rule::exists "$rule" || return 0
|
||||||
|
|
||||||
|
if [[ -n "$identity_rules" ]]; then
|
||||||
|
# Both identity and peer rules exist — show peer block with same pattern
|
||||||
|
printf "\n \033[0;37m· peer:%s\033[0m\n" "$name"
|
||||||
|
ui::rule::_peer_rule_entry "$rule"
|
||||||
|
else
|
||||||
|
# Only peer rule — render directly without peer: label
|
||||||
|
printf "\n"
|
||||||
|
if rule::render_extends_tree "$rule"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
rule::render_flat "$rule"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif [[ "$strict" == "true" && -n "$rule" ]]; then
|
||||||
|
printf "\n \033[2mpeer rule '%s' suppressed by strict policy\033[0m\n" "$rule"
|
||||||
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::on_load() {
|
function cmd::list::on_load() {
|
||||||
|
load_module identity
|
||||||
|
load_module ui
|
||||||
|
|
||||||
flag::register --type
|
flag::register --type
|
||||||
flag::register --rule
|
flag::register --rule
|
||||||
flag::register --group
|
flag::register --group
|
||||||
|
|
@ -29,87 +32,29 @@ Usage: wgctl list [options]
|
||||||
List all WireGuard clients.
|
List all WireGuard clients.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--type <type> Filter by device type (desktop, laptop, phone, tablet)
|
--type <type> Filter by device type
|
||||||
--rule <rule> Filter by assigned rule
|
--rule <rule> Filter by assigned rule
|
||||||
--group <group> Filter by group membership
|
--group <group> Filter by group membership
|
||||||
--identity <name> Filter by identity (show all peers for an identity)
|
--identity <name> Filter by identity
|
||||||
--online Show only connected clients
|
--online Show only connected clients
|
||||||
--offline Show only disconnected clients
|
--offline Show only disconnected clients
|
||||||
--blocked Show only fully blocked clients (removed from WireGuard)
|
--blocked Show only fully blocked clients
|
||||||
--restricted Show only restricted clients (specific IP/port blocks applied)
|
--restricted Show only restricted clients
|
||||||
--allowed Show only unrestricted clients
|
--allowed Show only unrestricted clients
|
||||||
--detailed Show full detail cards for all clients
|
--detailed Show detailed view grouped by identity
|
||||||
--name <name> Show detail card for a single client
|
--name <name> Show detail card for a single client
|
||||||
|
|
||||||
Status values:
|
|
||||||
online Connected (recent handshake)
|
|
||||||
offline Not connected
|
|
||||||
blocked Removed from WireGuard server (wgctl block --name)
|
|
||||||
restricted In WireGuard but with specific access rules (wgctl block --ip/--service)
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl list
|
wgctl list
|
||||||
wgctl list --type phone
|
wgctl list --type phone
|
||||||
wgctl list --rule user
|
|
||||||
wgctl list --group family
|
|
||||||
wgctl list --identity nuno
|
wgctl list --identity nuno
|
||||||
wgctl list --online
|
wgctl list --online
|
||||||
wgctl list --blocked
|
wgctl list --blocked
|
||||||
wgctl list --restricted
|
|
||||||
wgctl list --detailed
|
wgctl list --detailed
|
||||||
wgctl list --name phone-nuno
|
wgctl list --name phone-nuno
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Header / Footer
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
function cmd::list::_render_header() {
|
|
||||||
local has_groups="$1"
|
|
||||||
if $has_groups; then
|
|
||||||
printf "\n %-28s %-15s %-13s %-12s %-12s %-22s %s\n" \
|
|
||||||
"NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..135})"
|
|
||||||
else
|
|
||||||
printf "\n %-28s %-15s %-13s %-12s %-22s %s\n" \
|
|
||||||
"NAME" "IP" "TYPE" "RULE" "STATUS" "LAST SEEN"
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..107})"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::list::_render_footer() {
|
|
||||||
local has_groups="$1"
|
|
||||||
if $has_groups; then
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..135})"
|
|
||||||
else
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..107})"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::list::_render_summary() {
|
|
||||||
local group_summary="${1:-}"
|
|
||||||
local -n _rule_counts="$2"
|
|
||||||
local filter_desc="${3:-}"
|
|
||||||
|
|
||||||
local total=0
|
|
||||||
for r in "${!_rule_counts[@]}"; do
|
|
||||||
(( total += _rule_counts[$r] )) || true
|
|
||||||
done
|
|
||||||
|
|
||||||
local summary=""
|
|
||||||
for r in "${!_rule_counts[@]}"; do
|
|
||||||
summary+="${_rule_counts[$r]} ${r}, "
|
|
||||||
done
|
|
||||||
summary="${summary%, }"
|
|
||||||
|
|
||||||
if [[ -n "$group_summary" ]]; then
|
|
||||||
printf "\n Showing %s peers [%s] — %s\n\n" "$total" "$summary" "$group_summary"
|
|
||||||
else
|
|
||||||
printf "\n Showing %s peers [%s]\n\n" "$total" "$summary"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Detail Card
|
# Detail Card
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -133,7 +78,6 @@ function cmd::list::show_client() {
|
||||||
local public_key
|
local public_key
|
||||||
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
# Meta type is authoritative; IP reverse lookup is fallback for pre-migration peers
|
|
||||||
local type
|
local type
|
||||||
type=$(peers::get_meta "$name" "type" 2>/dev/null)
|
type=$(peers::get_meta "$name" "type" 2>/dev/null)
|
||||||
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
||||||
|
|
@ -241,36 +185,53 @@ function cmd::list::run() {
|
||||||
|
|
||||||
cmd::list::_precompute_all
|
cmd::list::_precompute_all
|
||||||
|
|
||||||
if $detailed; then
|
# Resolve identity filter
|
||||||
log::section "WireGuard Clients"
|
declare -gA p_identity_filter=()
|
||||||
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
|
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"
|
||||||
|
|
||||||
|
# Collect all filtered rows first (needed for dynamic column widths)
|
||||||
|
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
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local filter_desc=""
|
if $detailed; then
|
||||||
cmd::list::_build_filter_desc
|
cmd::list::_render_detailed "$collected_rows"
|
||||||
|
cmd::list::_render_summary_from_rows "$collected_rows"
|
||||||
declare -A rule_counts=() group_counts=()
|
return 0
|
||||||
_list_header_printed=false
|
|
||||||
|
|
||||||
cmd::list::_iter_confs "$filter_type" cmd::list::_render_row
|
|
||||||
|
|
||||||
if [[ "$_list_header_printed" == "true" ]]; then
|
|
||||||
cmd::list::_render_footer $has_groups
|
|
||||||
local group_summary=""
|
|
||||||
cmd::list::_build_group_summary
|
|
||||||
cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc"
|
|
||||||
else
|
|
||||||
log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local style
|
||||||
|
style=$(ui::peer::list_style)
|
||||||
|
|
||||||
|
case "$style" in
|
||||||
|
table) cmd::list::_render_table ;;
|
||||||
|
compact) cmd::list::_render_compact "$collected_rows" ;;
|
||||||
|
*) cmd::list::_render_compact "$collected_rows" ;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Iteration
|
# Row collection (single pass, all filters)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_iter_confs() {
|
function cmd::list::_collect_all_rows() {
|
||||||
local filter_type="$1" callback="$2"
|
# Outputs pipe-delimited rows for peers that pass all filters
|
||||||
|
# Fields: name|ip|type|rule|group|status|last_seen|is_blocked|is_restricted
|
||||||
local dir
|
local dir
|
||||||
dir="$(ctx::clients)"
|
dir="$(ctx::clients)"
|
||||||
|
|
||||||
|
|
@ -278,8 +239,124 @@ function cmd::list::_iter_confs() {
|
||||||
[[ -f "$conf" ]] || continue
|
[[ -f "$conf" ]] || continue
|
||||||
local client_name
|
local client_name
|
||||||
client_name=$(basename "$conf" .conf)
|
client_name=$(basename "$conf" .conf)
|
||||||
|
[[ -z "$client_name" ]] && continue
|
||||||
|
|
||||||
|
# Identity filter
|
||||||
|
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]:-}"
|
||||||
|
|
||||||
|
# Apply status filters
|
||||||
|
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
|
||||||
|
|
||||||
|
# Apply rule/group filters
|
||||||
|
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
|
||||||
|
local state
|
||||||
|
state=$(peers::connection_state "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
|
local status="${state%%|*}"
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
elif [[ -n "$handshake_ts" && "$handshake_ts" != "0" ]]; then
|
||||||
|
last_seen=$(fmt::datetime_short "$handshake_ts")
|
||||||
|
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() {
|
||||||
|
declare -A rule_counts=() group_counts=()
|
||||||
|
_list_header_printed=false
|
||||||
|
|
||||||
|
cmd::list::_iter_confs_table
|
||||||
|
|
||||||
|
if [[ "$_list_header_printed" == "true" ]]; then
|
||||||
|
cmd::list::_render_footer $has_groups
|
||||||
|
local group_summary=""
|
||||||
|
cmd::list::_build_group_summary
|
||||||
|
printf "\n Showing peers\n\n"
|
||||||
|
else
|
||||||
|
log::wg_warning "No results found"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
# Identity filter — skip peers not in the identity set
|
|
||||||
if [[ ${#p_identity_filter[@]} -gt 0 && \
|
if [[ ${#p_identity_filter[@]} -gt 0 && \
|
||||||
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
||||||
continue
|
continue
|
||||||
|
|
@ -288,16 +365,111 @@ function cmd::list::_iter_confs() {
|
||||||
local ip="${p_ips[$client_name]:-}"
|
local ip="${p_ips[$client_name]:-}"
|
||||||
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
|
|
||||||
# p_types is authoritative — set during precompute from meta + IP fallback
|
|
||||||
local type="${p_types[$client_name]:-unknown}"
|
local type="${p_types[$client_name]:-unknown}"
|
||||||
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||||
|
|
||||||
"$callback" "$client_name" "$ip" "$type"
|
cmd::list::_render_row "$client_name" "$ip" "$type"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Row rendering
|
# 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 subnet
|
||||||
|
subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null) || subnet="-"
|
||||||
|
[[ -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 < <(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) || subnet="—"
|
||||||
|
[[ -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=()
|
||||||
|
local total=0
|
||||||
|
|
||||||
|
while IFS='|' read -r name ip type rule rest; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
(( total++ )) || true
|
||||||
|
rule_counts["${rule:-—}"]=$(( ${rule_counts[${rule:-—}]:-0} + 1 )) || true
|
||||||
|
done <<< "$rows"
|
||||||
|
|
||||||
|
local summary=""
|
||||||
|
for r in "${!rule_counts[@]}"; do
|
||||||
|
summary+="${rule_counts[$r]} ${r}, "
|
||||||
|
done
|
||||||
|
summary="${summary%, }"
|
||||||
|
|
||||||
|
printf " Showing %s peers [%s]\n\n" "$total" "$summary"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Table row rendering
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_render_row() {
|
function cmd::list::_render_row() {
|
||||||
|
|
@ -321,7 +493,7 @@ function cmd::list::_render_row() {
|
||||||
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local status last_seen display_type rule group_display
|
local status last_seen display_type rule
|
||||||
status=$(peers::format_status_verbose "$client_name" "$pubkey" \
|
status=$(peers::format_status_verbose "$client_name" "$pubkey" \
|
||||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
||||||
|
|
@ -332,7 +504,6 @@ function cmd::list::_render_row() {
|
||||||
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
||||||
|
|
||||||
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
||||||
log::section "WireGuard Clients"
|
|
||||||
cmd::list::_render_header $has_groups
|
cmd::list::_render_header $has_groups
|
||||||
_list_header_printed=true
|
_list_header_printed=true
|
||||||
fi
|
fi
|
||||||
|
|
@ -344,26 +515,12 @@ function cmd::list::_render_row() {
|
||||||
|
|
||||||
if $has_groups; then
|
if $has_groups; then
|
||||||
local main_group="${p_main_groups[$client_name]:-}"
|
local main_group="${p_main_groups[$client_name]:-}"
|
||||||
if [[ -n "$main_group" ]]; then
|
local group_display="${main_group:-${peer_group_map[$client_name]:-—}}"
|
||||||
group_display="$main_group"
|
printf " %-28s %-15s %-13s %-12s %-12s %s %s\n" \
|
||||||
else
|
|
||||||
group_display="${peer_group_map[$client_name]:-—}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
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" \
|
"$client_name" "$ip" "$display_type" "$rule" \
|
||||||
"$group_display" "$padded_status" "$last_seen"
|
"$group_display" "$padded_status" "$last_seen"
|
||||||
else
|
else
|
||||||
local rule_col_width=12
|
printf " %-28s %-15s %-13s %-12s %s %s\n" \
|
||||||
[[ "$rule" == "—" ]] && rule_col_width=14
|
|
||||||
printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \
|
|
||||||
"$client_name" "$ip" "$display_type" "$rule" \
|
"$client_name" "$ip" "$display_type" "$rule" \
|
||||||
"$padded_status" "$last_seen"
|
"$padded_status" "$last_seen"
|
||||||
fi
|
fi
|
||||||
|
|
@ -374,25 +531,22 @@ function cmd::list::_render_row() {
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_precompute_all() {
|
function cmd::list::_precompute_all() {
|
||||||
# Peer data — field 4 is 'type' from peer_data_v2
|
|
||||||
declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=()
|
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
|
while IFS="|" read -r name ip rule type 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_types["$name"]="${type:-}"
|
p_types["$name"]="${type:-}"
|
||||||
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:-}"
|
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)")
|
||||||
|
|
||||||
# Fill type from IP for peers missing meta type (pre-migration peers)
|
|
||||||
for name in "${!p_ips[@]}"; do
|
for name in "${!p_ips[@]}"; do
|
||||||
[[ -n "${p_types[$name]:-}" ]] && continue
|
[[ -n "${p_types[$name]:-}" ]] && continue
|
||||||
p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
|
p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
|
||||||
done
|
done
|
||||||
|
|
||||||
# WireGuard handshakes + endpoints
|
|
||||||
declare -gA wg_handshakes=() wg_endpoints=()
|
declare -gA wg_handshakes=() wg_endpoints=()
|
||||||
while IFS=$'\t' read -r pubkey ts; do
|
while IFS=$'\t' read -r pubkey ts; do
|
||||||
[[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
[[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
||||||
|
|
@ -401,11 +555,9 @@ function cmd::list::_precompute_all() {
|
||||||
[[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
[[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
||||||
done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
||||||
|
|
||||||
# Block/restricted status
|
|
||||||
declare -gA p_blocked=() p_restricted=()
|
declare -gA p_blocked=() p_restricted=()
|
||||||
cmd::list::_precompute_block_status p_blocked p_restricted
|
cmd::list::_precompute_block_status p_blocked p_restricted
|
||||||
|
|
||||||
# Public keys
|
|
||||||
declare -gA p_pubkeys=()
|
declare -gA p_pubkeys=()
|
||||||
local dir
|
local dir
|
||||||
dir="$(ctx::clients)"
|
dir="$(ctx::clients)"
|
||||||
|
|
@ -416,7 +568,6 @@ 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 + main group
|
|
||||||
has_groups=false
|
has_groups=false
|
||||||
declare -gA peer_group_map=()
|
declare -gA peer_group_map=()
|
||||||
local groups_dir
|
local groups_dir
|
||||||
|
|
@ -429,27 +580,8 @@ function cmd::list::_precompute_all() {
|
||||||
done < <(json::peer_group_map "$groups_dir")
|
done < <(json::peer_group_map "$groups_dir")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Resolve identity filter into a peer set
|
# Identity precompute (for --identity filter)
|
||||||
declare -gA p_identity_filter=()
|
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
|
|
||||||
|
|
||||||
# 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() {
|
||||||
|
|
@ -477,19 +609,15 @@ function cmd::list::_precompute_block_status() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Filter helpers
|
# Header / Footer (table layout)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_build_filter_desc() {
|
function cmd::list::_render_header() {
|
||||||
filter_desc=""
|
ui::peer::list_header_table "$1"
|
||||||
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
}
|
||||||
[[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
|
|
||||||
[[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
|
function cmd::list::_render_footer() {
|
||||||
[[ -n "$filter_identity" ]] && filter_desc+="identity=${filter_identity} "
|
ui::peer::list_footer_table "$1"
|
||||||
$online_only && filter_desc+="online "
|
|
||||||
$offline_only && filter_desc+="offline "
|
|
||||||
$blocked_only && filter_desc+="blocked "
|
|
||||||
filter_desc="${filter_desc% }"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::list::_build_group_summary() {
|
function cmd::list::_build_group_summary() {
|
||||||
|
|
|
||||||
|
|
@ -100,24 +100,17 @@ function cmd::policy::run() {
|
||||||
function cmd::policy::_list() {
|
function cmd::policy::_list() {
|
||||||
local data
|
local data
|
||||||
data=$(policy::list_data)
|
data=$(policy::list_data)
|
||||||
|
|
||||||
if [[ -z "$data" ]]; then
|
if [[ -z "$data" ]]; then
|
||||||
log::info "No policies defined."
|
log::info "No policies defined."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf " %-14s %-8s %-14s %-12s %-12s %s\n" \
|
echo ""
|
||||||
"NAME" "TUNNEL" "DEFAULT RULE" "STRICT RULE" "AUTO APPLY" "DESCRIPTION"
|
|
||||||
ui::divider 84
|
|
||||||
|
|
||||||
while IFS='|' read -r name tunnel default_rule strict auto desc; do
|
while IFS='|' read -r name tunnel default_rule strict auto desc; do
|
||||||
local rule_display="${default_rule:-—}"
|
ui::policy::list_row "$name" "$default_rule" "$strict" "$auto"
|
||||||
local strict_display auto_display
|
|
||||||
[[ "$strict" == "true" ]] && strict_display="$(color::red yes)" || strict_display="no"
|
|
||||||
[[ "$auto" == "true" ]] && auto_display="yes" || auto_display="no"
|
|
||||||
printf " %-14s %-8s %-14s %-12s %-12s %s\n" \
|
|
||||||
"$name" "$tunnel" "$rule_display" "$strict_display" "$auto_display" "$desc"
|
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::policy::_show() {
|
function cmd::policy::_show() {
|
||||||
|
|
@ -129,18 +122,34 @@ function cmd::policy::_show() {
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||||
policy::require_exists "$name" || return 1
|
policy::require_exists "$name" || return 1
|
||||||
|
|
||||||
echo ""
|
local dr tunnel strict auto
|
||||||
ui::row "Name" "$name"
|
|
||||||
ui::row "Tunnel mode" "$(policy::tunnel_mode "$name")"
|
|
||||||
local dr
|
|
||||||
dr=$(policy::default_rule "$name")
|
dr=$(policy::default_rule "$name")
|
||||||
ui::row "Default rule" "${dr:-—}"
|
tunnel=$(policy::tunnel_mode "$name")
|
||||||
ui::row "Strict rule" "$(policy::strict_rule "$name" && echo "yes" || echo "no")"
|
strict=$(policy::strict_rule "$name" && echo "yes" || echo "no")
|
||||||
ui::row "Auto apply" "$(policy::auto_apply "$name" && echo "yes" || echo "no")"
|
auto=$(policy::auto_apply "$name" && echo "yes" || echo "no")
|
||||||
|
|
||||||
|
local rule_val="-"
|
||||||
|
[[ -n "$dr" ]] && rule_val="$dr"
|
||||||
|
|
||||||
|
local strict_padded
|
||||||
|
strict_padded=$(printf "%-4s" "$strict")
|
||||||
|
|
||||||
|
# First line — mirrors list format
|
||||||
|
echo ""
|
||||||
|
printf " \033[1m%-14s\033[0m \033[2mrule:\033[0m %-16s \033[2mstrict:\033[0m %s\n" \
|
||||||
|
"$name" "$rule_val" "$strict_padded"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detail section
|
||||||
|
local desc
|
||||||
|
desc=$(policy::get "$name" "desc")
|
||||||
|
[[ -n "$desc" ]] && printf " \033[2mDescription:\033[0m %s\n" "$desc"
|
||||||
|
printf " \033[2mTunnel:\033[0m %s\n" "$tunnel"
|
||||||
|
printf " \033[2mAuto apply:\033[0m %s\n" "$auto"
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -358,13 +358,15 @@ function cmd::rule::show() {
|
||||||
ui::row "Group" "${group:-—}"
|
ui::row "Group" "${group:-—}"
|
||||||
ui::row "DNS" "$dns_display"
|
ui::row "DNS" "$dns_display"
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
# ── Extends + own rules ────────────────────────
|
# ── Extends + own rules ────────────────────────
|
||||||
if rule::render_extends_tree "$name"; then
|
if rule::render_extends_tree "$name"; then
|
||||||
# Has inheritance — tree already rendered
|
# Has inheritance — tree already rendered
|
||||||
printf "\n"
|
:
|
||||||
else
|
else
|
||||||
# No inheritance — flat view
|
# No inheritance — flat view
|
||||||
rule::render_flat "$name"
|
rule::render_flat "$name"
|
||||||
|
printf "\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Resolved ──────────────────────────────────
|
# ── Resolved ──────────────────────────────────
|
||||||
|
|
@ -381,7 +383,6 @@ function cmd::rule::show() {
|
||||||
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \
|
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \
|
||||||
<<< "$res_block_ips"$'\n'"$res_block_ports"
|
<<< "$res_block_ips"$'\n'"$res_block_ports"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Peers ─────────────────────────────────────
|
# ── Peers ─────────────────────────────────────
|
||||||
|
|
@ -389,7 +390,10 @@ function cmd::rule::show() {
|
||||||
mapfile -t peer_list < <(peers::with_rule "$name")
|
mapfile -t peer_list < <(peers::with_rule "$name")
|
||||||
|
|
||||||
local peer_count=${#peer_list[@]}
|
local peer_count=${#peer_list[@]}
|
||||||
|
|
||||||
|
ui::empty "$peer_count" && return 0
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
printf "\033[0;37m── Peers (%s) \033[0m%s\n\n" \
|
printf "\033[0;37m── Peers (%s) \033[0m%s\n\n" \
|
||||||
"$(color::gray "${peer_count}")" \
|
"$(color::gray "${peer_count}")" \
|
||||||
"$(printf '\033[0;37m─%.0s' {1..35})"
|
"$(printf '\033[0;37m─%.0s' {1..35})"
|
||||||
|
|
|
||||||
|
|
@ -86,20 +86,31 @@ function cmd::subnet::run() {
|
||||||
function cmd::subnet::_list() {
|
function cmd::subnet::_list() {
|
||||||
local data
|
local data
|
||||||
data=$(subnet::list_data)
|
data=$(subnet::list_data)
|
||||||
|
|
||||||
if [[ -z "$data" ]]; then
|
if [[ -z "$data" ]]; then
|
||||||
log::info "No subnets defined."
|
log::info "No subnets defined."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ui::subnet::header
|
echo ""
|
||||||
|
|
||||||
local prev_group=""
|
local prev_group=""
|
||||||
while IFS='|' read -r display_name subnet type_key tunnel_mode desc is_group group_parent; do
|
while IFS='|' read -r display_name subnet type_key tunnel_mode desc is_group group_parent; do
|
||||||
cmd::subnet::_maybe_group_separator "$is_group" "$group_parent" "$prev_group"
|
if [[ "$is_group" == "true" ]]; then
|
||||||
prev_group=$(cmd::subnet::_update_prev_group "$is_group" "$group_parent" "$prev_group")
|
# Print group parent header when we encounter first child
|
||||||
ui::subnet::row "$display_name" "$subnet" "$type_key" "$tunnel_mode" "$desc" "$is_group"
|
if [[ "$group_parent" != "$prev_group" ]]; then
|
||||||
|
[[ -n "$prev_group" ]] && ui::subnet::group_separator
|
||||||
|
ui::subnet::row_group_parent "$group_parent"
|
||||||
|
prev_group="$group_parent"
|
||||||
|
fi
|
||||||
|
ui::subnet::row_group_child "$type_key" "$subnet" "$tunnel_mode"
|
||||||
|
else
|
||||||
|
# Scalar entry
|
||||||
|
[[ -n "$prev_group" ]] && ui::subnet::group_separator
|
||||||
|
prev_group=""
|
||||||
|
ui::subnet::row_scalar "$display_name" "$subnet" "$tunnel_mode"
|
||||||
|
fi
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::subnet::_maybe_group_separator() {
|
function cmd::subnet::_maybe_group_separator() {
|
||||||
|
|
@ -129,42 +140,53 @@ function cmd::subnet::_show() {
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||||
subnet::require_exists "$name" || return 1
|
subnet::require_exists "$name" || return 1
|
||||||
|
|
||||||
local data
|
local data
|
||||||
data=$(subnet::show_data "$name")
|
data=$(subnet::show_data "$name")
|
||||||
|
|
||||||
local is_group="false"
|
local is_group="false"
|
||||||
|
local show_name="" show_subnet="" show_tunnel="" show_desc=""
|
||||||
|
|
||||||
while IFS='|' read -r key val rest; do
|
while IFS='|' read -r key val rest; do
|
||||||
case "$key" in
|
case "$key" in
|
||||||
name)
|
name) show_name="$val" ;;
|
||||||
ui::subnet::detail "$val" "$is_group"
|
is_group) is_group="$val" ;;
|
||||||
;;
|
subnet) show_subnet="$val" ;;
|
||||||
is_group)
|
tunnel_mode) show_tunnel="$val" ;;
|
||||||
is_group="$val"
|
desc) show_desc="$val" ;;
|
||||||
ui::subnet::detail_field "Type" "$( [[ $val == true ]] && echo "group" || echo "scalar" )"
|
|
||||||
[[ "$val" == "true" ]] && ui::subnet::child_header
|
|
||||||
;;
|
|
||||||
subnet) ui::subnet::detail_field "Subnet" "$val" ;;
|
|
||||||
type) ui::subnet::detail_field "Device type" "$val" ;;
|
|
||||||
tunnel_mode) ui::subnet::detail_field "Tunnel" "$val" ;;
|
|
||||||
desc) ui::subnet::detail_field "Description" "$val" ;;
|
|
||||||
child)
|
|
||||||
local c_type="$val"
|
|
||||||
local c_subnet c_tunnel c_desc
|
|
||||||
c_subnet=$(echo "$rest" | cut -d'|' -f1)
|
|
||||||
c_tunnel=$(echo "$rest" | cut -d'|' -f2)
|
|
||||||
c_desc=$(echo "$rest" | cut -d'|' -f3)
|
|
||||||
ui::subnet::child_row "$c_type" "$c_subnet" "$c_tunnel" "$c_desc"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
|
|
||||||
local peers_using
|
if [[ "$is_group" == "true" ]]; then
|
||||||
peers_using=$(subnet::peers_using "$name")
|
# Group display
|
||||||
ui::subnet::peers_in_use "$peers_using"
|
ui::subnet::show_group "$show_name"
|
||||||
|
|
||||||
|
while IFS='|' read -r key val rest; do
|
||||||
|
[[ "$key" != "child" ]] && continue
|
||||||
|
local c_type="$val"
|
||||||
|
local c_subnet c_tunnel c_desc
|
||||||
|
c_subnet=$(echo "$rest" | cut -d'|' -f1)
|
||||||
|
c_tunnel=$(echo "$rest" | cut -d'|' -f2)
|
||||||
|
c_desc=$(echo "$rest" | cut -d'|' -f3)
|
||||||
|
ui::subnet::show_child_row "$c_type" "$c_subnet" "$c_tunnel" "$c_desc"
|
||||||
|
done <<< "$data"
|
||||||
|
|
||||||
|
local peers_using
|
||||||
|
peers_using=$(subnet::peers_using "$name")
|
||||||
|
ui::subnet::show_peers_annotated "$peers_using" "$(ctx::subnets)"
|
||||||
|
else
|
||||||
|
# Scalar display
|
||||||
|
ui::subnet::show_scalar "$show_name" "$show_subnet" "$show_tunnel" "$show_desc"
|
||||||
|
|
||||||
|
local peers_using
|
||||||
|
peers_using=$(subnet::peers_using "$name")
|
||||||
|
ui::subnet::show_peers "$peers_using"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::subnet::_add() {
|
function cmd::subnet::_add() {
|
||||||
|
|
|
||||||
|
|
@ -136,12 +136,12 @@ function cmd::test::run_all_integration_sections() {
|
||||||
|
|
||||||
function cmd::test::section_list() {
|
function cmd::test::section_list() {
|
||||||
test::section "List"
|
test::section "List"
|
||||||
cmd::test::run_cmd "list" "NAME" list
|
cmd::test::run_cmd "list" "rule:" list
|
||||||
cmd::test::run_cmd "list --online" "" list --online
|
cmd::test::run_cmd "list --online" "" list --online
|
||||||
cmd::test::run_cmd "list --offline" "" list --offline
|
cmd::test::run_cmd "list --offline" "" list --offline
|
||||||
cmd::test::run_cmd "list --blocked" "" list --blocked
|
cmd::test::run_cmd "list --blocked" "" list --blocked
|
||||||
cmd::test::run_cmd "list --type phone" "phone" list --type phone
|
cmd::test::run_cmd "list --type phone" "phone" list --type phone
|
||||||
cmd::test::run_cmd "list --detailed" "IP:" list --detailed
|
cmd::test::run_cmd "list --detailed" "rule:" list --detailed
|
||||||
cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
|
cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,8 +225,8 @@ function cmd::test::section_subnet() {
|
||||||
"$WGCTL_BINARY" subnet rm --name test-subnet > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" subnet rm --name test-subnet > /dev/null 2>&1 || true
|
||||||
|
|
||||||
cmd::test::run_cmd "subnet list" "desktop" subnet list
|
cmd::test::run_cmd "subnet list" "desktop" subnet list
|
||||||
cmd::test::run_cmd "subnet show desktop" "Subnet" subnet show --name desktop
|
cmd::test::run_cmd "subnet show desktop" "tunnel:" subnet show --name desktop
|
||||||
cmd::test::run_cmd "subnet show guests group" "group" subnet show --name guests
|
cmd::test::run_cmd "subnet show guests group" "guests" subnet show --name guests
|
||||||
cmd::test::run_cmd_fails "subnet show nonexistent" subnet show --name nonexistent
|
cmd::test::run_cmd_fails "subnet show nonexistent" subnet show --name nonexistent
|
||||||
|
|
||||||
cmd::test::run_cmd "subnet add" "added" \
|
cmd::test::run_cmd "subnet add" "added" \
|
||||||
|
|
|
||||||
22
core/fmt.sh
22
core/fmt.sh
|
|
@ -34,6 +34,28 @@ function fmt::datetime_iso() {
|
||||||
python3 "$FMT_HELPER" fmt_datetime "$iso" "$FMT_DATETIME" </dev/null
|
python3 "$FMT_HELPER" fmt_datetime "$iso" "$FMT_DATETIME" </dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# fmt::datetime_short <unix_timestamp>
|
||||||
|
# Returns a compact datetime — just time if today, short date+time if older.
|
||||||
|
# Respects configured date format. Returns "—" for empty/zero timestamps.
|
||||||
|
function fmt::datetime_short() {
|
||||||
|
local ts="${1:-}"
|
||||||
|
[[ -z "$ts" || "$ts" == "0" ]] && echo "—" && return 0
|
||||||
|
|
||||||
|
local today ts_day
|
||||||
|
today=$(date +%Y-%m-%d)
|
||||||
|
ts_day=$(date -d "@${ts}" +%Y-%m-%d 2>/dev/null) || { echo "—"; return 0; }
|
||||||
|
|
||||||
|
if [[ "$ts_day" == "$today" ]]; then
|
||||||
|
date -d "@${ts}" +"%H:%M" 2>/dev/null || echo "—"
|
||||||
|
else
|
||||||
|
case "$_FMT_DATE_FORMAT" in
|
||||||
|
iso) date -d "@${ts}" +"%m-%d %H:%M" 2>/dev/null || echo "—" ;;
|
||||||
|
eu*) date -d "@${ts}" +"%d/%m %H:%M" 2>/dev/null || echo "—" ;;
|
||||||
|
*) date -d "@${ts}" +"%m-%d %H:%M" 2>/dev/null || echo "—" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
function fmt::set_date_format() {
|
function fmt::set_date_format() {
|
||||||
local format="$1"
|
local format="$1"
|
||||||
case "$format" in
|
case "$format" in
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,13 @@ function json::identity_migrate() { python3 "$JSON_HELPER" identity_migrat
|
||||||
function json::identity_infer() { python3 "$JSON_HELPER" identity_infer "$@" </dev/null; }
|
function json::identity_infer() { python3 "$JSON_HELPER" identity_infer "$@" </dev/null; }
|
||||||
function json::identity_exists() { python3 "$JSON_HELPER" identity_exists "$@" </dev/null; }
|
function json::identity_exists() { python3 "$JSON_HELPER" identity_exists "$@" </dev/null; }
|
||||||
|
|
||||||
|
# Identity rule wrappers (1:N)
|
||||||
|
function json::identity_rules() { python3 "$JSON_HELPER" identity_rules "$@" </dev/null; }
|
||||||
|
function json::identity_add_rule() { python3 "$JSON_HELPER" identity_add_rule "$@" </dev/null; }
|
||||||
|
function json::identity_remove_rule() { python3 "$JSON_HELPER" identity_remove_rule "$@" </dev/null; }
|
||||||
|
function json::identity_clear_rules() { python3 "$JSON_HELPER" identity_clear_rules "$@" </dev/null; }
|
||||||
|
function json::identity_has_rule() { python3 "$JSON_HELPER" identity_has_rule "$@" </dev/null; }
|
||||||
|
|
||||||
# Policy wrappers — append to json.sh
|
# Policy wrappers — append to json.sh
|
||||||
function json::policy_get() { python3 "$JSON_HELPER" policy_get "$@" </dev/null; }
|
function json::policy_get() { python3 "$JSON_HELPER" policy_get "$@" </dev/null; }
|
||||||
function json::policy_list() { python3 "$JSON_HELPER" policy_list "$@" </dev/null; }
|
function json::policy_list() { python3 "$JSON_HELPER" policy_list "$@" </dev/null; }
|
||||||
|
|
|
||||||
|
|
@ -1764,6 +1764,79 @@ def subnet_exists(file, name):
|
||||||
# Identity System
|
# Identity System
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
def identity_rules(file):
|
||||||
|
"""
|
||||||
|
Return all rules assigned to an identity, one per line.
|
||||||
|
Reads from 'rules' array (1:N). Falls back to 'rule' scalar for migration.
|
||||||
|
"""
|
||||||
|
data = _identity_read(file)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
# Support legacy scalar 'rule' field
|
||||||
|
rules = data.get('rules', [])
|
||||||
|
if not rules and data.get('rule'):
|
||||||
|
rules = [data['rule']]
|
||||||
|
for r in rules:
|
||||||
|
if r:
|
||||||
|
print(r)
|
||||||
|
|
||||||
|
def identity_add_rule(file, identity_name, rule_name):
|
||||||
|
"""
|
||||||
|
Add a rule to an identity's rules array.
|
||||||
|
Warns if already present (prints warning to stderr, exits 2).
|
||||||
|
Creates identity file if it doesn't exist.
|
||||||
|
"""
|
||||||
|
data = _identity_read(file) or _identity_init(identity_name)
|
||||||
|
rules = data.get('rules', [])
|
||||||
|
# Migrate legacy scalar field
|
||||||
|
if 'rule' in data and data['rule']:
|
||||||
|
if data['rule'] not in rules:
|
||||||
|
rules.append(data['rule'])
|
||||||
|
del data['rule']
|
||||||
|
if rule_name in rules:
|
||||||
|
print(f"Warning: Rule '{rule_name}' is already assigned to identity '{identity_name}'",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
rules.append(rule_name)
|
||||||
|
data['rules'] = rules
|
||||||
|
_identity_write(file, data)
|
||||||
|
|
||||||
|
def identity_remove_rule(file, rule_name):
|
||||||
|
"""
|
||||||
|
Remove a specific rule from an identity's rules array.
|
||||||
|
Exits 1 if rule not found.
|
||||||
|
"""
|
||||||
|
data = _identity_read(file)
|
||||||
|
if not data:
|
||||||
|
print(f"Error: Identity not found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
rules = data.get('rules', [])
|
||||||
|
if rule_name not in rules:
|
||||||
|
print(f"Error: Rule '{rule_name}' not assigned to this identity", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
rules.remove(rule_name)
|
||||||
|
data['rules'] = rules
|
||||||
|
_identity_write(file, data)
|
||||||
|
|
||||||
|
def identity_clear_rules(file):
|
||||||
|
"""Remove all rules from an identity."""
|
||||||
|
data = _identity_read(file)
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
data['rules'] = []
|
||||||
|
data.pop('rule', None) # remove legacy scalar too
|
||||||
|
_identity_write(file, data)
|
||||||
|
|
||||||
|
def identity_has_rule(file, rule_name):
|
||||||
|
"""Exit 0 if identity has this rule, 1 otherwise."""
|
||||||
|
data = _identity_read(file)
|
||||||
|
if not data:
|
||||||
|
sys.exit(1)
|
||||||
|
rules = data.get('rules', [])
|
||||||
|
if not rules and data.get('rule'):
|
||||||
|
rules = [data['rule']]
|
||||||
|
sys.exit(0 if rule_name in rules else 1)
|
||||||
|
|
||||||
def _identity_read(file):
|
def _identity_read(file):
|
||||||
"""Read an identity file, return dict or None"""
|
"""Read an identity file, return dict or None"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -1822,17 +1895,27 @@ def _parse_peer_name(peer_name):
|
||||||
return (peer_type, identity, index)
|
return (peer_type, identity, index)
|
||||||
|
|
||||||
def identity_list(identities_dir):
|
def identity_list(identities_dir):
|
||||||
"""List all identities with peer count"""
|
"""
|
||||||
|
List all identities with peer count, rules and policy.
|
||||||
|
Output per line: name|peer_count|types|rules|policy
|
||||||
|
"""
|
||||||
import glob
|
import glob
|
||||||
for id_file in sorted(glob.glob(f"{identities_dir}/*.identity")):
|
for id_file in sorted(glob.glob(f"{identities_dir}/*.identity")):
|
||||||
try:
|
try:
|
||||||
with open(id_file) as f:
|
with open(id_file) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
name = data.get('name', '')
|
name = data.get('name', '')
|
||||||
peers = data.get('peers', [])
|
peers = data.get('peers', [])
|
||||||
devices = data.get('devices', {})
|
devices = data.get('devices', {})
|
||||||
types = sorted(set(d.get('type', '') for d in devices.values() if d.get('type')))
|
rules = data.get('rules', [])
|
||||||
print(f"{name}|{len(peers)}|{','.join(types)}")
|
# Migrate legacy scalar rule field
|
||||||
|
if not rules and data.get('rule'):
|
||||||
|
rules = [data['rule']]
|
||||||
|
policy = data.get('policy', 'default')
|
||||||
|
types = sorted(set(
|
||||||
|
d.get('type', '') for d in devices.values() if d.get('type')
|
||||||
|
))
|
||||||
|
print(f"{name}|{len(peers)}|{','.join(types)}|{','.join(rules)}|{policy}")
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -2426,6 +2509,11 @@ commands = {
|
||||||
'subnet_policy': lambda args: subnet_policy(args[0], args[1], args[2] if len(args) > 2 else ''),
|
'subnet_policy': lambda args: subnet_policy(args[0], args[1], args[2] if len(args) > 2 else ''),
|
||||||
'get_nested': lambda args: json_get_nested(args[0], *args[1:]),
|
'get_nested': lambda args: json_get_nested(args[0], *args[1:]),
|
||||||
'set_nested': lambda args: json_set_nested(args[0], *args[1:]),
|
'set_nested': lambda args: json_set_nested(args[0], *args[1:]),
|
||||||
|
'identity_rules': lambda args: identity_rules(args[0]),
|
||||||
|
'identity_add_rule': lambda args: identity_add_rule(args[0], args[1], args[2]),
|
||||||
|
'identity_remove_rule': lambda args: identity_remove_rule(args[0], args[1]),
|
||||||
|
'identity_clear_rules': lambda args: identity_clear_rules(args[0]),
|
||||||
|
'identity_has_rule': lambda args: identity_has_rule(args[0], args[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
58
core/ui.sh
58
core/ui.sh
|
|
@ -77,6 +77,48 @@ function ui::center() {
|
||||||
printf "%${pad}s%s%${rpad}s" "" "$text" ""
|
printf "%${pad}s%s%${rpad}s" "" "$text" ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ui::measure_col <data> <field_index> [min_width]
|
||||||
|
# Scans pipe-delimited data and returns the max visible width
|
||||||
|
# of the field at field_index (1-based), with optional minimum.
|
||||||
|
# Strips ANSI codes before measuring.
|
||||||
|
# Usage:
|
||||||
|
# name_width=$(ui::measure_col "$data" 1 10)
|
||||||
|
# ip_width=$(ui::measure_col "$data" 2 14)
|
||||||
|
function ui::measure_col() {
|
||||||
|
local data="${1:-}" field_index="${2:-1}" min_width="${3:-0}"
|
||||||
|
local max=$min_width
|
||||||
|
|
||||||
|
while IFS='|' read -r line; do
|
||||||
|
local val
|
||||||
|
val=$(echo "$line" | cut -d'|' -f"$field_index")
|
||||||
|
# Strip ANSI codes for accurate measurement
|
||||||
|
local clean
|
||||||
|
clean=$(echo "$val" | sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
local len=${#clean}
|
||||||
|
(( len > max )) && max=$len
|
||||||
|
done <<< "$data"
|
||||||
|
|
||||||
|
echo $max
|
||||||
|
}
|
||||||
|
|
||||||
|
# ui::measure_cols <data> <field_indices...>
|
||||||
|
# Measure multiple columns at once, returns space-separated widths.
|
||||||
|
# Usage: read -r w1 w2 w3 <<< $(ui::measure_cols "$data" 1 2 3)
|
||||||
|
function ui::measure_cols() {
|
||||||
|
local data="${1:-}"
|
||||||
|
shift
|
||||||
|
local widths=()
|
||||||
|
for idx in "$@"; do
|
||||||
|
widths+=("$(ui::measure_col "$data" "$idx")")
|
||||||
|
done
|
||||||
|
echo "${widths[*]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::sort_rows() {
|
||||||
|
local field="${1:-1}"
|
||||||
|
sort -t'|' -k"${field},${field}V"
|
||||||
|
}
|
||||||
|
|
||||||
function ui::firewall_rule() {
|
function ui::firewall_rule() {
|
||||||
local rule="$1"
|
local rule="$1"
|
||||||
if [[ "$rule" =~ ACCEPT|DNAT ]]; then
|
if [[ "$rule" =~ ACCEPT|DNAT ]]; then
|
||||||
|
|
@ -109,9 +151,19 @@ function ui::skip_if_empty() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ui::empty() {
|
function ui::empty() {
|
||||||
# ui::empty "$var" && return 0
|
local val="${1:-}"
|
||||||
# ui::empty "${array[*]}" && return 0
|
# Empty string or whitespace only
|
||||||
[[ -z "${1// }" ]]
|
[[ -z "${val// }" ]] && return 0
|
||||||
|
# Numeric zero
|
||||||
|
[[ "$val" =~ ^[0-9]+$ ]] && [[ "$val" -eq 0 ]] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage: ui::bool "$value" [yes_label] [no_label]
|
||||||
|
# Default labels: yes / no
|
||||||
|
function ui::bool() {
|
||||||
|
local val="${1:-}" yes="${2:-yes}" no="${3:-no}"
|
||||||
|
[[ "$val" == "true" ]] && echo "$yes" || echo "$no"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
"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",
|
"phone-luis": "176.223.61.15",
|
||||||
"phone-helena-2": "148.69.192.157",
|
"phone-helena-2": "148.69.192.130",
|
||||||
"desktop-zephyr": "86.120.152.74"
|
"desktop-zephyr": "86.120.152.74"
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# identity.module.sh — identity file management and peer-name inference
|
# identity.module.sh — identity file management and peer-name inference
|
||||||
|
|
||||||
|
declare -gA __empty_map=()
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Path helpers
|
# Path helpers
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
@ -255,39 +257,7 @@ function identity::rename_peer() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# identity::rule <identity_name>
|
|
||||||
# Returns the rule assigned to an identity, or empty string if none.
|
|
||||||
function identity::rule() {
|
|
||||||
local identity_name="${1:-}"
|
|
||||||
local id_file
|
|
||||||
id_file=$(ctx::identity::path "${identity_name}.identity")
|
|
||||||
[[ ! -f "$id_file" ]] && return 0
|
|
||||||
json::get "$id_file" "rule" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# identity::set_rule <identity_name> <rule_name>
|
|
||||||
# Sets the rule on an identity file.
|
|
||||||
function identity::set_rule() {
|
|
||||||
local identity_name="${1:-}" rule_name="${2:-}"
|
|
||||||
local id_file
|
|
||||||
id_file=$(ctx::identity::path "${identity_name}.identity")
|
|
||||||
if [[ ! -f "$id_file" ]]; then
|
|
||||||
log::error "Identity '${identity_name}' not found"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
json::set "$id_file" "rule" "$rule_name"
|
|
||||||
}
|
|
||||||
|
|
||||||
# identity::clear_rule <identity_name>
|
|
||||||
# Removes the rule from an identity file.
|
|
||||||
function identity::clear_rule() {
|
|
||||||
local identity_name="${1:-}"
|
|
||||||
local id_file
|
|
||||||
id_file=$(ctx::identity::path "${identity_name}.identity")
|
|
||||||
[[ ! -f "$id_file" ]] && return 0
|
|
||||||
json::delete "$id_file" "rule" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# identity::policy <identity_name>
|
# identity::policy <identity_name>
|
||||||
# Returns the policy name assigned to an identity, or "default".
|
# Returns the policy name assigned to an identity, or "default".
|
||||||
function identity::policy() {
|
function identity::policy() {
|
||||||
|
|
@ -367,6 +337,82 @@ function identity::reapply_rules() {
|
||||||
peers=$(identity::peers "$identity_name")
|
peers=$(identity::peers "$identity_name")
|
||||||
[[ -z "$peers" ]] && return 0
|
[[ -z "$peers" ]] && return 0
|
||||||
|
|
||||||
|
while IFS= read -r peer_name; do
|
||||||
|
[[ -z "$peer_name" ]] && continue
|
||||||
|
local client_ip
|
||||||
|
client_ip=$(peers::get_ip "$peer_name") || continue
|
||||||
|
rule::full_restore_peer "$peer_name" "$client_ip"
|
||||||
|
done <<< "$peers"
|
||||||
|
}
|
||||||
|
|
||||||
|
# identity::rules <identity_name>
|
||||||
|
# Returns all rules assigned to an identity, one per line.
|
||||||
|
# Empty output if no rules assigned.
|
||||||
|
function identity::rules() {
|
||||||
|
local identity_name="${1:-}"
|
||||||
|
local id_file
|
||||||
|
id_file=$(ctx::identity::path "${identity_name}.identity")
|
||||||
|
[[ ! -f "$id_file" ]] && return 0
|
||||||
|
json::identity_rules "$id_file" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# identity::has_rule <identity_name> <rule_name>
|
||||||
|
# Returns 0 if identity has this rule, 1 otherwise.
|
||||||
|
function identity::has_rule() {
|
||||||
|
local identity_name="${1:-}" rule_name="${2:-}"
|
||||||
|
local id_file
|
||||||
|
id_file=$(ctx::identity::path "${identity_name}.identity")
|
||||||
|
json::identity_has_rule "$id_file" "$rule_name" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# identity::add_rule <identity_name> <rule_name>
|
||||||
|
# Adds a rule to an identity. Warns if already present (exit 2).
|
||||||
|
function identity::add_rule() {
|
||||||
|
local identity_name="${1:-}" rule_name="${2:-}"
|
||||||
|
local id_file
|
||||||
|
id_file=$(ctx::identity::path "${identity_name}.identity")
|
||||||
|
|
||||||
|
local exit_code=0
|
||||||
|
json::identity_add_rule "$id_file" "$identity_name" "$rule_name" 2>/dev/null || exit_code=$?
|
||||||
|
return $exit_code
|
||||||
|
}
|
||||||
|
|
||||||
|
# identity::remove_rule <identity_name> <rule_name>
|
||||||
|
# Removes a specific rule from an identity.
|
||||||
|
function identity::remove_rule() {
|
||||||
|
local identity_name="${1:-}" rule_name="${2:-}"
|
||||||
|
local id_file
|
||||||
|
id_file=$(ctx::identity::path "${identity_name}.identity")
|
||||||
|
json::identity_remove_rule "$id_file" "$rule_name" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# identity::clear_rules <identity_name>
|
||||||
|
# Removes all rules from an identity.
|
||||||
|
function identity::clear_rules() {
|
||||||
|
local identity_name="${1:-}"
|
||||||
|
local id_file
|
||||||
|
id_file=$(ctx::identity::path "${identity_name}.identity")
|
||||||
|
json::identity_clear_rules "$id_file" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# identity::reapply_rules <identity_name>
|
||||||
|
# Reapply all identity rules to all peers in this identity.
|
||||||
|
# Respects auto_apply flag.
|
||||||
|
function identity::reapply_rules() {
|
||||||
|
local identity_name="${1:-}"
|
||||||
|
|
||||||
|
local auto
|
||||||
|
auto=$(identity::rule_flags "$identity_name" "auto_apply")
|
||||||
|
[[ "$auto" == "false" ]] && return 0
|
||||||
|
|
||||||
|
local rules
|
||||||
|
rules=$(identity::rules "$identity_name")
|
||||||
|
[[ -z "$rules" ]] && return 0
|
||||||
|
|
||||||
|
local peers
|
||||||
|
peers=$(identity::peers "$identity_name")
|
||||||
|
[[ -z "$peers" ]] && return 0
|
||||||
|
|
||||||
while IFS= read -r peer_name; do
|
while IFS= read -r peer_name; do
|
||||||
[[ -z "$peer_name" ]] && continue
|
[[ -z "$peer_name" ]] && continue
|
||||||
local client_ip
|
local client_ip
|
||||||
|
|
|
||||||
|
|
@ -99,32 +99,21 @@ function rule::is_applied() {
|
||||||
# Rule Application
|
# Rule Application
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function rule::apply() {
|
function rule::_apply_entries() {
|
||||||
local rule_name="${1:?rule_name required}"
|
local rule_name="${1:?}" client_ip="${2:?}"
|
||||||
local client_ip="${2:?client_ip required}"
|
|
||||||
local peer_name="${3:-}"
|
|
||||||
|
|
||||||
rule::require_exists "$rule_name" || return 1
|
rule::require_exists "$rule_name" || return 1
|
||||||
|
|
||||||
if [[ -z "$peer_name" ]]; then
|
|
||||||
peer_name=$(peers::find_by_ip "$client_ip")
|
|
||||||
fi
|
|
||||||
|
|
||||||
log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
|
|
||||||
|
|
||||||
if rule::is_applied "$rule_name" "$client_ip"; then
|
if rule::is_applied "$rule_name" "$client_ip"; then
|
||||||
log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
|
log::debug "Rule '${rule_name}' already applied to: ${client_ip}"
|
||||||
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Process block_ips
|
|
||||||
while IFS= read -r block_ip; do
|
while IFS= read -r block_ip; do
|
||||||
[[ -z "$block_ip" ]] && continue
|
[[ -z "$block_ip" ]] && continue
|
||||||
fw::block_ip "$client_ip" "$block_ip"
|
fw::block_ip "$client_ip" "$block_ip"
|
||||||
done < <(rule::get "$rule_name" "block_ips")
|
done < <(rule::get "$rule_name" "block_ips")
|
||||||
|
|
||||||
# Process block_ports
|
|
||||||
while IFS= read -r entry; do
|
while IFS= read -r entry; do
|
||||||
[[ -z "$entry" ]] && continue
|
[[ -z "$entry" ]] && continue
|
||||||
local target port proto
|
local target port proto
|
||||||
|
|
@ -133,13 +122,11 @@ function rule::apply() {
|
||||||
fw::block_port "$client_ip" "$target" "$port" "$proto"
|
fw::block_port "$client_ip" "$target" "$port" "$proto"
|
||||||
done < <(rule::get "$rule_name" "block_ports")
|
done < <(rule::get "$rule_name" "block_ports")
|
||||||
|
|
||||||
# Process allow_ips (inserted before blocks)
|
|
||||||
while IFS= read -r allow_ip; do
|
while IFS= read -r allow_ip; do
|
||||||
[[ -z "$allow_ip" ]] && continue
|
[[ -z "$allow_ip" ]] && continue
|
||||||
fw::allow_ip "$client_ip" "$allow_ip"
|
fw::allow_ip "$client_ip" "$allow_ip"
|
||||||
done < <(rule::get "$rule_name" "allow_ips")
|
done < <(rule::get "$rule_name" "allow_ips")
|
||||||
|
|
||||||
# Process allow_ports (highest priority)
|
|
||||||
while IFS= read -r entry; do
|
while IFS= read -r entry; do
|
||||||
[[ -z "$entry" ]] && continue
|
[[ -z "$entry" ]] && continue
|
||||||
local target port proto
|
local target port proto
|
||||||
|
|
@ -148,27 +135,46 @@ function rule::apply() {
|
||||||
fw::allow_port "$client_ip" "$target" "$port" "$proto"
|
fw::allow_port "$client_ip" "$target" "$port" "$proto"
|
||||||
done < <(rule::get "$rule_name" "allow_ports")
|
done < <(rule::get "$rule_name" "allow_ports")
|
||||||
|
|
||||||
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
|
||||||
|
|
||||||
# DNS redirect
|
|
||||||
local dns_redirect
|
local dns_redirect
|
||||||
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
|
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
|
||||||
if [[ "$dns_redirect" == "true" ]]; then
|
if [[ "$dns_redirect" == "true" ]]; then
|
||||||
local peer_subnet
|
local peer_name peer_subnet
|
||||||
|
peer_name=$(peers::find_by_ip "$client_ip")
|
||||||
peer_subnet=$(peers::get_ip "$peer_name" | cut -d'.' -f1-3)
|
peer_subnet=$(peers::get_ip "$peer_name" | cut -d'.' -f1-3)
|
||||||
if ! fw::_nat_exists -i wg0 -s "${peer_subnet}.0/24" \
|
if ! fw::_nat_exists -i wg0 -s "${peer_subnet}.0/24" \
|
||||||
-p udp --dport 53 -j DNAT \
|
-p udp --dport 53 -j DNAT \
|
||||||
--to-destination "$(config::dns):53" 2>/dev/null; then
|
--to-destination "$(config::dns):53" 2>/dev/null; then
|
||||||
rule::apply_dns_redirect "${peer_subnet}.0/24"
|
rule::apply_dns_redirect "${peer_subnet}.0/24"
|
||||||
log::debug "dns_redirect: applied for ${peer_subnet}.0/24"
|
|
||||||
else
|
|
||||||
log::debug "dns_redirect: already applied for ${peer_subnet}.0/24"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log::debug "Applied rule '${rule_name}' to: ${client_ip}"
|
log::debug "Applied rule '${rule_name}' to: ${client_ip}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rule::apply_transient() {
|
||||||
|
# Apply rule entries without touching peer meta
|
||||||
|
# Used for identity rules and other transient applications
|
||||||
|
local rule_name="${1:?}" client_ip="${2:?}"
|
||||||
|
log::debug "rule::apply_transient: $rule_name -> $client_ip"
|
||||||
|
rule::_apply_entries "$rule_name" "$client_ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
function rule::apply() {
|
||||||
|
local rule_name="${1:?}" client_ip="${2:?}" peer_name="${3:-}"
|
||||||
|
|
||||||
|
rule::require_exists "$rule_name" || return 1
|
||||||
|
|
||||||
|
log::debug "rule::apply: peer_name=${peer_name:-<lookup>} ip=$client_ip"
|
||||||
|
|
||||||
|
rule::_apply_entries "$rule_name" "$client_ip" || return 1
|
||||||
|
|
||||||
|
# Write to peer meta — only for explicit peer rule assignment
|
||||||
|
if [[ -z "$peer_name" ]]; then
|
||||||
|
peer_name=$(peers::find_by_ip "$client_ip")
|
||||||
|
fi
|
||||||
|
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
||||||
|
}
|
||||||
|
|
||||||
function rule::unapply() {
|
function rule::unapply() {
|
||||||
local rule_name="${1:-}" client_ip="${2:-}"
|
local rule_name="${1:-}" client_ip="${2:-}"
|
||||||
|
|
||||||
|
|
@ -236,21 +242,31 @@ function rule::unapply() {
|
||||||
|
|
||||||
function rule::_apply_identity_rule() {
|
function rule::_apply_identity_rule() {
|
||||||
local peer_name="${1:-}" client_ip="${2:-}"
|
local peer_name="${1:-}" client_ip="${2:-}"
|
||||||
|
|
||||||
local identity_name
|
local identity_name
|
||||||
identity_name=$(identity::get_name "$peer_name")
|
identity_name=$(identity::get_name "$peer_name")
|
||||||
[[ -z "$identity_name" ]] && return 0
|
[[ -z "$identity_name" ]] && return 0
|
||||||
|
|
||||||
local identity_rule strict
|
local rules
|
||||||
identity_rule=$(identity::rule "$identity_name")
|
rules=$(identity::rules "$identity_name")
|
||||||
[[ -z "$identity_rule" ]] && return 0
|
[[ -z "$rules" ]] && return 0
|
||||||
|
|
||||||
|
local strict
|
||||||
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
||||||
|
|
||||||
if [[ "$strict" == "true" ]]; then
|
if [[ "$strict" == "true" ]]; then
|
||||||
|
# Strict: flush and apply only identity rules — peer rule ignored
|
||||||
fw::flush_peer "$client_ip"
|
fw::flush_peer "$client_ip"
|
||||||
rule::apply "$identity_rule" "$client_ip" "$peer_name"
|
while IFS= read -r rule_name; do
|
||||||
|
[[ -z "$rule_name" ]] && continue
|
||||||
|
rule::exists "$rule_name" && rule::apply_transient "$rule_name" "$client_ip" || true
|
||||||
|
done <<< "$rules"
|
||||||
else
|
else
|
||||||
rule::apply "$identity_rule" "$client_ip" "$peer_name"
|
# Additive: apply identity rules on top of peer rule
|
||||||
|
while IFS= read -r rule_name; do
|
||||||
|
[[ -z "$rule_name" ]] && continue
|
||||||
|
rule::exists "$rule_name" && rule::apply_transient "$rule_name" "$client_ip" || true
|
||||||
|
done <<< "$rules"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,35 +277,33 @@ function rule::_apply_identity_rule() {
|
||||||
function rule::full_restore_peer() {
|
function rule::full_restore_peer() {
|
||||||
local peer_name="${1:-}" client_ip="${2:-}"
|
local peer_name="${1:-}" client_ip="${2:-}"
|
||||||
[[ -z "$peer_name" || -z "$client_ip" ]] && return 1
|
[[ -z "$peer_name" || -z "$client_ip" ]] && return 1
|
||||||
|
|
||||||
fw::flush_peer "$client_ip"
|
fw::flush_peer "$client_ip"
|
||||||
|
|
||||||
local peer_rule
|
local peer_rule
|
||||||
peer_rule=$(peers::get_meta "$peer_name" "rule")
|
peer_rule=$(peers::get_meta "$peer_name" "rule")
|
||||||
[[ -n "$peer_rule" ]] && rule::apply "$peer_rule" "$client_ip" "$peer_name"
|
|
||||||
|
local strict
|
||||||
rule::_apply_identity_rule "$peer_name" "$client_ip"
|
strict=$(rule::_get_identity_strict "$peer_name")
|
||||||
|
|
||||||
|
if [[ "$strict" == "true" ]]; then
|
||||||
|
# Strict mode: only identity rules apply
|
||||||
|
rule::_apply_identity_rule "$peer_name" "$client_ip"
|
||||||
|
else
|
||||||
|
# Normal mode: peer rule + identity rules (additive)
|
||||||
|
[[ -n "$peer_rule" ]] && rule::apply "$peer_rule" "$client_ip" "$peer_name"
|
||||||
|
rule::_apply_identity_rule "$peer_name" "$client_ip"
|
||||||
|
fi
|
||||||
|
|
||||||
block::restore_rules_for "$peer_name" "$client_ip"
|
block::restore_rules_for "$peer_name" "$client_ip"
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::reapply_all() {
|
function rule::_get_identity_strict() {
|
||||||
local rule_name="${1:-}"
|
local peer_name="${1:-}"
|
||||||
rule::require_exists "$rule_name" || return 1
|
local identity_name
|
||||||
|
identity_name=$(identity::get_name "$peer_name")
|
||||||
local peers=()
|
[[ -z "$identity_name" ]] && echo "false" && return 0
|
||||||
mapfile -t peers < <(peers::with_rule "$rule_name")
|
identity::rule_flags "$identity_name" "strict_rule"
|
||||||
[[ ${#peers[@]} -eq 0 ]] && return 0
|
|
||||||
|
|
||||||
local count=0
|
|
||||||
for peer_name in "${peers[@]}"; do
|
|
||||||
local client_ip
|
|
||||||
client_ip=$(peers::get_ip "$peer_name")
|
|
||||||
[[ -z "$client_ip" ]] && continue
|
|
||||||
rule::full_restore_peer "$peer_name" "$client_ip"
|
|
||||||
(( count++ )) || true
|
|
||||||
done
|
|
||||||
|
|
||||||
log::wg_success "Rule '${rule_name}' re-applied to ${count} peers"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::restore_all() {
|
function rule::restore_all() {
|
||||||
|
|
@ -320,125 +334,19 @@ function rule::restore_all() {
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function rule::render_flat() {
|
function rule::render_flat() {
|
||||||
local rule_name="${1:-}"
|
ui::rule::flat "$1"
|
||||||
|
|
||||||
local allow_ports allow_ips block_ips block_ports dns
|
|
||||||
allow_ports=$(rule::get "$rule_name" "allow_ports")
|
|
||||||
allow_ips=$(rule::get "$rule_name" "allow_ips")
|
|
||||||
block_ips=$(rule::get "$rule_name" "block_ips")
|
|
||||||
block_ports=$(rule::get "$rule_name" "block_ports")
|
|
||||||
dns=$(rule::get_own "$rule_name" "dns_redirect")
|
|
||||||
|
|
||||||
local has_content=false
|
|
||||||
[[ -n "${allow_ports}${allow_ips}${block_ips}${block_ports}" ]] && \
|
|
||||||
has_content=true
|
|
||||||
|
|
||||||
if ! $has_content; then
|
|
||||||
printf "\n full access (no restrictions)\n"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
|
|
||||||
printf "\n"
|
|
||||||
while IFS= read -r e; do
|
|
||||||
[[ -z "$e" ]] && continue
|
|
||||||
net::print_entry "+" "$e" 2
|
|
||||||
done <<< "$allow_ports"$'\n'"$allow_ips"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$block_ips" || -n "$block_ports" ]]; then
|
|
||||||
printf "\n"
|
|
||||||
while IFS= read -r e; do
|
|
||||||
[[ -z "$e" ]] && continue
|
|
||||||
net::print_entry "-" "$e" 2
|
|
||||||
done <<< "$block_ips"$'\n'"$block_ports"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ "${dns,,}" == "true" ]] && \
|
|
||||||
net::print_dns_redirect "$(config::dns)" 6 "DNS"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::render_entries() {
|
function rule::render_entries() {
|
||||||
local rule_name="${1:-}" indent="${2:-4}"
|
ui::rule::entries "$1"
|
||||||
|
|
||||||
local allow_ports allow_ips block_ips block_ports dns
|
|
||||||
allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true)
|
|
||||||
allow_ips=$(rule::get "$rule_name" "allow_ips" 2>/dev/null || true)
|
|
||||||
block_ips=$(rule::get "$rule_name" "block_ips" 2>/dev/null || true)
|
|
||||||
block_ports=$(rule::get "$rule_name" "block_ports" 2>/dev/null || true)
|
|
||||||
dns=$(rule::get_own "$rule_name" "dns_redirect")
|
|
||||||
|
|
||||||
while IFS= read -r e; do
|
|
||||||
[[ -z "$e" ]] && continue
|
|
||||||
net::print_entry "+" "$e"
|
|
||||||
done <<< "$allow_ports"$'\n'"$allow_ips"
|
|
||||||
|
|
||||||
while IFS= read -r e; do
|
|
||||||
[[ -z "$e" ]] && continue
|
|
||||||
net::print_entry "-" "$e"
|
|
||||||
done <<< "$block_ips"$'\n'"$block_ports"
|
|
||||||
|
|
||||||
[[ "${dns,,}" == "true" ]] && \
|
|
||||||
net::print_dns_redirect "$(config::dns)" 6 "DNS"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::render_own_entries() {
|
function rule::render_own_entries() {
|
||||||
local rule_name="${1:-}"
|
ui::rule::own_entries "$1"
|
||||||
local rule_file
|
|
||||||
rule_file="$(rule::path "$rule_name")"
|
|
||||||
|
|
||||||
local allow_ports allow_ips block_ips block_ports dns
|
|
||||||
allow_ports=$(json::get "$rule_file" "allow_ports" 2>/dev/null || true)
|
|
||||||
allow_ips=$(json::get "$rule_file" "allow_ips" 2>/dev/null || true)
|
|
||||||
block_ips=$(json::get "$rule_file" "block_ips" 2>/dev/null || true)
|
|
||||||
block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true)
|
|
||||||
dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true)
|
|
||||||
|
|
||||||
local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}"
|
|
||||||
[[ -z "${combined//[$'\n']/}" ]] && return 0
|
|
||||||
|
|
||||||
while IFS= read -r e; do
|
|
||||||
[[ -z "$e" ]] && continue
|
|
||||||
net::print_entry "+" "$e"
|
|
||||||
done <<< "$allow_ports"$'\n'"$allow_ips"
|
|
||||||
|
|
||||||
while IFS= read -r e; do
|
|
||||||
[[ -z "$e" ]] && continue
|
|
||||||
net::print_entry "-" "$e"
|
|
||||||
done <<< "$block_ips"$'\n'"$block_ports"
|
|
||||||
|
|
||||||
[[ "${dns,,}" == "true" ]] && \
|
|
||||||
net::print_dns_redirect "$(config::dns)" 6 "DNS"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::render_extends_tree() {
|
function rule::render_extends_tree() {
|
||||||
local rule_name="${1:-}"
|
ui::rule::tree "$1"
|
||||||
local rule_file
|
|
||||||
rule_file="$(rule::path "$rule_name")"
|
|
||||||
|
|
||||||
local extends_raw=()
|
|
||||||
mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true)
|
|
||||||
|
|
||||||
[[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]:-}" ]] && return 1
|
|
||||||
|
|
||||||
for base_name in "${extends_raw[@]}"; do
|
|
||||||
[[ -z "$base_name" ]] && continue
|
|
||||||
printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name"
|
|
||||||
rule::render_entries "$base_name"
|
|
||||||
done
|
|
||||||
|
|
||||||
local own_output
|
|
||||||
own_output=$(rule::render_own_entries "$rule_name")
|
|
||||||
if [[ -n "$own_output" ]]; then
|
|
||||||
printf "\n \033[0;37mOwn:\033[0m\n"
|
|
||||||
printf "%s\n" "$own_output"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,30 @@ function ui::identity::migrate_summary() {
|
||||||
else
|
else
|
||||||
log::ok "Created identity entries for ${created} peers (${skipped} skipped)"
|
log::ok "Created identity entries for ${created} peers (${skipped} skipped)"
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::identity::list_row_compact() {
|
||||||
|
local name="${1:-}" peer_count="${2:-}" rules_list="${3:-}" policy="${4:-}"
|
||||||
|
local peer_word="peers"
|
||||||
|
[[ "$peer_count" -eq 1 ]] && peer_word="peer"
|
||||||
|
|
||||||
|
local peers_col="${peer_count} ${peer_word}"
|
||||||
|
local rules_val="-"
|
||||||
|
[[ -n "$rules_list" ]] && rules_val="$rules_list"
|
||||||
|
|
||||||
|
# Pad rules_val to fixed width before adding any ANSI
|
||||||
|
local pad=16
|
||||||
|
local rules_padded
|
||||||
|
rules_padded=$(printf "%-${pad}s" "$rules_val")
|
||||||
|
|
||||||
|
printf " \033[1m%-20s\033[0m %-10s \033[2mrules:\033[0m %s \033[2mpolicy:\033[0m %s\n" \
|
||||||
|
"$name" "$peers_col" "$rules_padded" "$policy"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ui::identity::list_row_table() {
|
||||||
|
local name="${1:-}" peer_count="${2:-}" types="${3:-}"
|
||||||
|
local types_display="${types//,/, }"
|
||||||
|
[[ -z "$types_display" ]] && types_display="—"
|
||||||
|
printf " %-20s %-7s %s\n" "$name" "$peer_count" "$types_display"
|
||||||
}
|
}
|
||||||
161
modules/ui/peer.module.sh
Normal file
161
modules/ui/peer.module.sh
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ui/peer.module.sh — rendering for peer list data
|
||||||
|
# Both compact (tableless) and table layouts kept for future config switching.
|
||||||
|
|
||||||
|
_LIST_STYLE="${LIST_STYLE:-compact}"
|
||||||
|
|
||||||
|
function ui::peer::list_style() {
|
||||||
|
echo "$_LIST_STYLE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Compact layout (tableless)
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function ui::peer::list_row_compact() {
|
||||||
|
local w_name="${1:-22}" w_ip="${2:-14}" w_type="${3:-9}" \
|
||||||
|
w_rule="${4:-6}" w_group="${5:-6}"
|
||||||
|
shift 5
|
||||||
|
local name="${1:-}" ip="${2:-}" type="${3:-}" rule="${4:-}" \
|
||||||
|
group="${5:-}" status="${6:-}" last_seen="${7:-}" \
|
||||||
|
is_blocked="${8:-false}" is_restricted="${9:-false}"
|
||||||
|
|
||||||
|
local status_color="\033[0;37m"
|
||||||
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
|
status_color="\033[1;31m"
|
||||||
|
elif [[ "$is_restricted" == "true" ]]; then
|
||||||
|
status_color="\033[1;33m"
|
||||||
|
elif [[ "$status" == "online" ]]; then
|
||||||
|
status_color="\033[1;32m"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ls_color="\033[0;37m"
|
||||||
|
[[ "$status" == "online" ]] && ls_color="\033[1;32m"
|
||||||
|
|
||||||
|
local rule_val="${rule:--}"
|
||||||
|
local group_val="${group:--}"
|
||||||
|
|
||||||
|
# Pad name, ip, type — pure ASCII, safe for printf
|
||||||
|
local name_pad ip_pad type_pad status_pad
|
||||||
|
name_pad=$(printf "%-${w_name}s" "$name")
|
||||||
|
ip_pad=$(printf "%-${w_ip}s" "$ip")
|
||||||
|
type_pad=$(printf "%-${w_type}s" "$type")
|
||||||
|
status_pad=$(printf "%-8s" "$status")
|
||||||
|
|
||||||
|
# Padding for label+value fields — compute trailing spaces manually
|
||||||
|
# so ANSI codes in labels don't confuse printf width calculation
|
||||||
|
local rule_pad_n group_pad_n
|
||||||
|
rule_pad_n=$(( w_rule - ${#rule_val} ))
|
||||||
|
group_pad_n=$(( w_group - ${#group_val} ))
|
||||||
|
[[ $rule_pad_n -lt 0 ]] && rule_pad_n=0
|
||||||
|
[[ $group_pad_n -lt 0 ]] && group_pad_n=0
|
||||||
|
|
||||||
|
printf " %s %s %s \033[2mrule:\033[0m %s%*s \033[2mgroup:\033[0m %s%*s %b%s\033[0m %b%s\033[0m\n" \
|
||||||
|
"$name_pad" "$ip_pad" "$type_pad" \
|
||||||
|
"$rule_val" "$rule_pad_n" "" \
|
||||||
|
"$group_val" "$group_pad_n" "" \
|
||||||
|
"$status_color" "$status_pad" \
|
||||||
|
"$ls_color" "$last_seen"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Table layout (kept for config switching)
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function ui::peer::list_header_table() {
|
||||||
|
local has_groups="${1:-false}"
|
||||||
|
if $has_groups; then
|
||||||
|
printf "\n %-28s %-15s %-13s %-12s %-12s %-22s %s\n" \
|
||||||
|
"NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..135})"
|
||||||
|
else
|
||||||
|
printf "\n %-28s %-15s %-13s %-12s %-22s %s\n" \
|
||||||
|
"NAME" "IP" "TYPE" "RULE" "STATUS" "LAST SEEN"
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..107})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::peer::list_footer_table() {
|
||||||
|
local has_groups="${1:-false}"
|
||||||
|
if $has_groups; then
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..135})"
|
||||||
|
else
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..107})"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::peer::list_row_table() {
|
||||||
|
local has_groups="${1:-false}"
|
||||||
|
shift
|
||||||
|
local name="${1:-}" ip="${2:-}" type="${3:-}" rule="${4:-}" \
|
||||||
|
group="${5:-}" status="${6:-}" last_seen="${7:-}"
|
||||||
|
|
||||||
|
local padded_status
|
||||||
|
padded_status=$(ui::pad_status "$status" 25)
|
||||||
|
|
||||||
|
if $has_groups; then
|
||||||
|
printf " %-28s %-15s %-13s %-12s %-12s %s %s\n" \
|
||||||
|
"$name" "$ip" "$type" "${rule:-—}" "${group:-—}" \
|
||||||
|
"$padded_status" "$last_seen"
|
||||||
|
else
|
||||||
|
printf " %-28s %-15s %-13s %-12s %s %s\n" \
|
||||||
|
"$name" "$ip" "$type" "${rule:-—}" \
|
||||||
|
"$padded_status" "$last_seen"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Detailed layout (grouped by identity)
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function ui::peer::list_identity_header() {
|
||||||
|
local identity_name="${1:-}"
|
||||||
|
printf "\n \033[1m%s\033[0m\n" "$identity_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::peer::list_row_detailed() {
|
||||||
|
local w_name="${1:-22}" w_ip="${2:-14}" w_type="${3:-9}" \
|
||||||
|
w_rule="${4:-6}" w_group="${5:-6}" w_subnet="${6:-8}"
|
||||||
|
shift 6
|
||||||
|
local name="${1:-}" ip="${2:-}" type="${3:-}" rule="${4:-}" \
|
||||||
|
group="${5:-}" subnet="${6:-}" status="${7:-}" last_seen="${8:-}" \
|
||||||
|
is_blocked="${9:-false}" is_restricted="${10:-false}"
|
||||||
|
|
||||||
|
local status_color="\033[0;37m"
|
||||||
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
|
status_color="\033[1;31m"
|
||||||
|
elif [[ "$is_restricted" == "true" ]]; then
|
||||||
|
status_color="\033[1;33m"
|
||||||
|
elif [[ "$status" == "online" ]]; then
|
||||||
|
status_color="\033[1;32m"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ls_color="\033[0;37m"
|
||||||
|
[[ "$status" == "online" ]] && ls_color="\033[1;32m"
|
||||||
|
|
||||||
|
local rule_val="${rule:-—}"
|
||||||
|
local group_val="${group:-—}"
|
||||||
|
local subnet_val="${subnet:-—}"
|
||||||
|
|
||||||
|
local name_pad ip_pad type_pad status_pad
|
||||||
|
name_pad=$(printf "%-${w_name}s" "$name")
|
||||||
|
ip_pad=$(printf "%-${w_ip}s" "$ip")
|
||||||
|
type_pad=$(printf "%-${w_type}s" "$type")
|
||||||
|
status_pad=$(printf "%-8s" "$status")
|
||||||
|
|
||||||
|
local rule_pad_n group_pad_n subnet_pad_n
|
||||||
|
rule_pad_n=$(( w_rule - ${#rule_val} ))
|
||||||
|
group_pad_n=$(( w_group - ${#group_val} ))
|
||||||
|
subnet_pad_n=$(( w_subnet - ${#subnet_val} ))
|
||||||
|
[[ $rule_pad_n -lt 0 ]] && rule_pad_n=0
|
||||||
|
[[ $group_pad_n -lt 0 ]] && group_pad_n=0
|
||||||
|
[[ $subnet_pad_n -lt 0 ]] && subnet_pad_n=0
|
||||||
|
|
||||||
|
printf " · %s %s %s \033[2mrule:\033[0m %s%*s \033[2mgroup:\033[0m %s%*s \033[2msubnet:\033[0m %s%*s %b%s\033[0m %b%s\033[0m\n" \
|
||||||
|
"$name_pad" "$ip_pad" "$type_pad" \
|
||||||
|
"$rule_val" "$rule_pad_n" "" \
|
||||||
|
"$group_val" "$group_pad_n" "" \
|
||||||
|
"$subnet_val" "$subnet_pad_n" "" \
|
||||||
|
"$status_color" "$status_pad" \
|
||||||
|
"$ls_color" "$last_seen"
|
||||||
|
}
|
||||||
257
modules/ui/rule.module.sh
Normal file
257
modules/ui/rule.module.sh
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ui/rule.module.sh — rendering for rule data
|
||||||
|
# Replaces rule::render_* functions from rule.module.sh.
|
||||||
|
# All functions pure rendering — no writes, no state changes.
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Entry Rendering (shared primitives)
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
# ui::rule::entries <rule_name> [indent]
|
||||||
|
# Renders the fully resolved entries for a rule (allow + block).
|
||||||
|
function ui::rule::entries() {
|
||||||
|
local rule_name="${1:-}" indent="${2:-4}"
|
||||||
|
|
||||||
|
local allow_ports allow_ips block_ips block_ports dns
|
||||||
|
allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true)
|
||||||
|
allow_ips=$(rule::get "$rule_name" "allow_ips" 2>/dev/null || true)
|
||||||
|
block_ips=$(rule::get "$rule_name" "block_ips" 2>/dev/null || true)
|
||||||
|
block_ports=$(rule::get "$rule_name" "block_ports" 2>/dev/null || true)
|
||||||
|
dns=$(rule::get_own "$rule_name" "dns_redirect")
|
||||||
|
|
||||||
|
while IFS= read -r e; do
|
||||||
|
[[ -z "$e" ]] && continue
|
||||||
|
net::print_entry "+" "$e" "$indent"
|
||||||
|
done <<< "$allow_ports"$'\n'"$allow_ips"
|
||||||
|
|
||||||
|
while IFS= read -r e; do
|
||||||
|
[[ -z "$e" ]] && continue
|
||||||
|
net::print_entry "-" "$e" "$indent"
|
||||||
|
done <<< "$block_ips"$'\n'"$block_ports"
|
||||||
|
|
||||||
|
[[ "${dns,,}" == "true" ]] && \
|
||||||
|
net::print_dns_redirect "$(config::dns)" 6 "DNS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ui::rule::own_entries <rule_name> [indent]
|
||||||
|
# Renders only the rule's own (non-inherited) entries.
|
||||||
|
function ui::rule::own_entries() {
|
||||||
|
local rule_name="${1:-}" indent="${2:-4}"
|
||||||
|
local rule_file
|
||||||
|
rule_file="$(rule::path "$rule_name")" || return 0
|
||||||
|
[[ -z "$rule_file" ]] && return 0
|
||||||
|
|
||||||
|
local allow_ports allow_ips block_ips block_ports dns
|
||||||
|
allow_ports=$(json::get "$rule_file" "allow_ports" 2>/dev/null || true)
|
||||||
|
allow_ips=$(json::get "$rule_file" "allow_ips" 2>/dev/null || true)
|
||||||
|
block_ips=$(json::get "$rule_file" "block_ips" 2>/dev/null || true)
|
||||||
|
block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true)
|
||||||
|
dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true)
|
||||||
|
|
||||||
|
local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}"
|
||||||
|
[[ -z "${combined//[$'\n']/}" ]] && return 0
|
||||||
|
|
||||||
|
while IFS= read -r e; do
|
||||||
|
[[ -z "$e" ]] && continue
|
||||||
|
net::print_entry "+" "$e" "$indent"
|
||||||
|
done <<< "$allow_ports"$'\n'"$allow_ips"
|
||||||
|
|
||||||
|
while IFS= read -r e; do
|
||||||
|
[[ -z "$e" ]] && continue
|
||||||
|
net::print_entry "-" "$e" "$indent"
|
||||||
|
done <<< "$block_ips"$'\n'"$block_ports"
|
||||||
|
|
||||||
|
[[ "${dns,,}" == "true" ]] && \
|
||||||
|
net::print_dns_redirect "$(config::dns)" 6 "DNS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ui::rule::flat <rule_name>
|
||||||
|
# Renders the full resolved entries as a flat list.
|
||||||
|
function ui::rule::flat() {
|
||||||
|
local rule_name="${1:-}"
|
||||||
|
|
||||||
|
local allow_ports allow_ips block_ips block_ports dns
|
||||||
|
allow_ports=$(rule::get "$rule_name" "allow_ports")
|
||||||
|
allow_ips=$(rule::get "$rule_name" "allow_ips")
|
||||||
|
block_ips=$(rule::get "$rule_name" "block_ips")
|
||||||
|
block_ports=$(rule::get "$rule_name" "block_ports")
|
||||||
|
dns=$(rule::get_own "$rule_name" "dns_redirect")
|
||||||
|
|
||||||
|
local has_content=false
|
||||||
|
[[ -n "${allow_ports}${allow_ips}${block_ips}${block_ports}" ]] && has_content=true
|
||||||
|
|
||||||
|
if ! $has_content; then
|
||||||
|
printf "\n full access (no restrictions)\n"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
|
||||||
|
printf "\n"
|
||||||
|
while IFS= read -r e; do
|
||||||
|
[[ -z "$e" ]] && continue
|
||||||
|
net::print_entry "+" "$e" 2
|
||||||
|
done <<< "$allow_ports"$'\n'"$allow_ips"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$block_ips" || -n "$block_ports" ]]; then
|
||||||
|
printf "\n"
|
||||||
|
while IFS= read -r e; do
|
||||||
|
[[ -z "$e" ]] && continue
|
||||||
|
net::print_entry "-" "$e" 2
|
||||||
|
done <<< "$block_ips"$'\n'"$block_ports"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ "${dns,,}" == "true" ]] && \
|
||||||
|
net::print_dns_redirect "$(config::dns)" 6 "DNS"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Shared Base Renderer
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
# ui::rule::_render_bases <array_nameref> [entry_indent] [label_indent]
|
||||||
|
# Renders a list of base rule names with newlines between them.
|
||||||
|
# Used by both ui::rule::tree and ui::rule::_identity_rule_entry.
|
||||||
|
function ui::rule::_render_bases() {
|
||||||
|
local -n _bases="$1"
|
||||||
|
local entry_indent="${2:-6}" label_indent="${3:-4}"
|
||||||
|
local label_pad
|
||||||
|
label_pad=$(printf '%*s' "$label_indent" '')
|
||||||
|
|
||||||
|
local first=true
|
||||||
|
for base_name in "${_bases[@]}"; do
|
||||||
|
[[ -z "$base_name" ]] && continue
|
||||||
|
$first || printf "\n"
|
||||||
|
first=false
|
||||||
|
printf "%s\033[0;37m↳ %s\033[0m\n" "$label_pad" "$base_name"
|
||||||
|
ui::rule::entries "$base_name" "$entry_indent"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Tree Rendering
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
# ui::rule::tree <rule_name>
|
||||||
|
# Renders a rule's extends tree — one level deep with own entries.
|
||||||
|
# Returns 1 if rule has no extends (caller can fall back to flat).
|
||||||
|
function ui::rule::tree() {
|
||||||
|
local rule_name="${1:-}"
|
||||||
|
local rule_file
|
||||||
|
rule_file="$(rule::path "$rule_name")" || return 1
|
||||||
|
[[ -z "$rule_file" ]] && return 1
|
||||||
|
|
||||||
|
local extends_raw=()
|
||||||
|
mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) || true
|
||||||
|
|
||||||
|
if [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]:-}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui::rule::_render_bases extends_raw 6 4
|
||||||
|
|
||||||
|
local own_output
|
||||||
|
own_output=$(ui::rule::own_entries "$rule_name" 6)
|
||||||
|
if [[ -n "$own_output" ]]; then
|
||||||
|
printf "\n \033[0;37mOwn:\033[0m\n"
|
||||||
|
printf "%s\n" "$own_output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Identity Rule Block
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
# ui::rule::identity_block <identity_name> <strict_rule>
|
||||||
|
# Renders the full identity rule block in inspect.
|
||||||
|
function ui::rule::identity_block() {
|
||||||
|
local identity_name="${1:-}" strict="${2:-false}"
|
||||||
|
|
||||||
|
local rules
|
||||||
|
rules=$(identity::rules "$identity_name")
|
||||||
|
[[ -z "$rules" ]] && return 0
|
||||||
|
|
||||||
|
printf "\n \033[0;37m· identity:%s\033[0m\n" "$identity_name"
|
||||||
|
|
||||||
|
local first=true
|
||||||
|
while IFS= read -r rule_name; do
|
||||||
|
[[ -z "$rule_name" ]] && continue
|
||||||
|
$first || printf "\n"
|
||||||
|
first=false
|
||||||
|
ui::rule::_identity_rule_entry "$rule_name"
|
||||||
|
done <<< "$rules"
|
||||||
|
|
||||||
|
if [[ "$strict" == "true" ]]; then
|
||||||
|
printf "\n \033[2m(strict — peer rule suppressed)\033[0m\n"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ui::rule::_identity_rule_entry <rule_name>
|
||||||
|
# Renders one rule within an identity block.
|
||||||
|
function ui::rule::_identity_rule_entry() {
|
||||||
|
local rule_name="${1:-}"
|
||||||
|
local rule_file
|
||||||
|
rule_file="$(rule::path "$rule_name")" || return 0
|
||||||
|
|
||||||
|
printf " \033[0;37m↳ %s\033[0m\n" "$rule_name"
|
||||||
|
|
||||||
|
local extends_raw=()
|
||||||
|
mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) || true
|
||||||
|
|
||||||
|
if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then
|
||||||
|
# Rule has extends — render one level deep using shared helper
|
||||||
|
ui::rule::_render_bases extends_raw 10 8
|
||||||
|
|
||||||
|
local own_output
|
||||||
|
own_output=$(ui::rule::own_entries "$rule_name" 10)
|
||||||
|
if [[ -n "$own_output" ]]; then
|
||||||
|
printf "\n \033[0;37mOwn:\033[0m\n"
|
||||||
|
printf "%s\n" "$own_output"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Leaf rule — show own entries or note full access
|
||||||
|
local own_output
|
||||||
|
own_output=$(ui::rule::own_entries "$rule_name" 8)
|
||||||
|
if [[ -n "$own_output" ]]; then
|
||||||
|
printf "%s\n" "$own_output"
|
||||||
|
else
|
||||||
|
printf " \033[2mfull access (no restrictions)\033[0m\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Peer Entry
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function ui::rule::_peer_rule_entry() {
|
||||||
|
local rule_name="${1:-}"
|
||||||
|
local rule_file
|
||||||
|
rule_file="$(rule::path "$rule_name")" || return 0
|
||||||
|
|
||||||
|
printf " \033[0;37m↳ %s\033[0m\n" "$rule_name"
|
||||||
|
|
||||||
|
local extends_raw=()
|
||||||
|
mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) || true
|
||||||
|
|
||||||
|
if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then
|
||||||
|
ui::rule::_render_bases extends_raw 10 8
|
||||||
|
|
||||||
|
local own_output
|
||||||
|
own_output=$(ui::rule::own_entries "$rule_name" 10)
|
||||||
|
if [[ -n "$own_output" ]]; then
|
||||||
|
printf "\n \033[0;37mOwn:\033[0m\n"
|
||||||
|
printf "%s\n" "$own_output"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
local own_output
|
||||||
|
own_output=$(ui::rule::own_entries "$rule_name" 8)
|
||||||
|
if [[ -n "$own_output" ]]; then
|
||||||
|
printf "%s\n" "$own_output"
|
||||||
|
else
|
||||||
|
printf " \033[2mfull access (no restrictions)\033[0m\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,33 @@ function ui::subnet::header() {
|
||||||
ui::divider 70
|
ui::divider 70
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ui::policy::list_row() {
|
||||||
|
local name="${1:-}" default_rule="${2:-}" strict="${3:-}" auto="${4:-}"
|
||||||
|
|
||||||
|
local rule_val="-"
|
||||||
|
[[ -n "$default_rule" ]] && rule_val="$default_rule"
|
||||||
|
|
||||||
|
local rule_padded
|
||||||
|
rule_padded=$(printf "%-16s" "$rule_val")
|
||||||
|
|
||||||
|
local strict_display
|
||||||
|
[[ "$strict" == "true" ]] && strict_display="yes" || strict_display="no"
|
||||||
|
local strict_padded
|
||||||
|
strict_padded=$(printf "%-4s" "$strict_display")
|
||||||
|
|
||||||
|
local auto_display=""
|
||||||
|
[[ "$auto" == "false" ]] && auto_display=" \033[2mauto:\033[0m no"
|
||||||
|
|
||||||
|
printf " %-14s \033[2mrule:\033[0m %s \033[2mstrict:\033[0m %s%s\n" \
|
||||||
|
"$name" "$rule_padded" "$strict_padded" "$auto_display"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::policy::detail_field() {
|
||||||
|
local key="${1:-}" value="${2:-}"
|
||||||
|
ui::row "$key" "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function ui::subnet::row() {
|
function ui::subnet::row() {
|
||||||
local display_name="${1:-}" subnet="${2:-}" type_key="${3:-}" \
|
local display_name="${1:-}" subnet="${2:-}" type_key="${3:-}" \
|
||||||
tunnel_mode="${4:-}" desc="${5:-}" is_group="${6:-false}"
|
tunnel_mode="${4:-}" desc="${5:-}" is_group="${6:-false}"
|
||||||
|
|
@ -17,6 +44,27 @@ function ui::subnet::row() {
|
||||||
"$name_col" "$subnet" "$type_key" "$tunnel_mode" "$desc"
|
"$name_col" "$subnet" "$type_key" "$tunnel_mode" "$desc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ui::subnet::row_scalar() {
|
||||||
|
local name="${1:-}" subnet="${2:-}" tunnel="${3:-split}"
|
||||||
|
local tunnel_val
|
||||||
|
tunnel_val=$(printf "%-8s" "$tunnel")
|
||||||
|
printf " %-14s %-18s \033[2mtunnel:\033[0m %s\n" \
|
||||||
|
"$name" "$subnet" "$tunnel_val"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::subnet::row_group_parent() {
|
||||||
|
local name="${1:-}"
|
||||||
|
printf " \033[1m%s\033[0m\n" "$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::subnet::row_group_child() {
|
||||||
|
local type_key="${1:-}" subnet="${2:-}" tunnel="${3:-split}"
|
||||||
|
local tunnel_val
|
||||||
|
tunnel_val=$(printf "%-8s" "$tunnel")
|
||||||
|
printf " · %-10s %-18s \033[2mtunnel:\033[0m %s\n" \
|
||||||
|
"$type_key" "$subnet" "$tunnel_val"
|
||||||
|
}
|
||||||
|
|
||||||
function ui::subnet::group_separator() {
|
function ui::subnet::group_separator() {
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
@ -48,4 +96,118 @@ function ui::subnet::peers_in_use() {
|
||||||
[[ -z "$peers_csv" ]] && return 0
|
[[ -z "$peers_csv" ]] && return 0
|
||||||
echo ""
|
echo ""
|
||||||
ui::row "Peers using" "${peers_csv//,/, }"
|
ui::row "Peers using" "${peers_csv//,/, }"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::subnet::show_scalar() {
|
||||||
|
local name="${1:-}" subnet="${2:-}" tunnel="${3:-}" desc="${4:-}"
|
||||||
|
echo ""
|
||||||
|
printf " \033[1m%-16s\033[0m %-18s \033[2mtunnel:\033[0m %s\n" \
|
||||||
|
"$name" "$subnet" "$tunnel"
|
||||||
|
echo ""
|
||||||
|
[[ -n "$desc" ]] && printf " \033[2mDescription:\033[0m %s\n" "$desc"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::subnet::show_group() {
|
||||||
|
local name="${1:-}"
|
||||||
|
echo ""
|
||||||
|
printf " \033[1m%s\033[0m\n" "$name"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::subnet::show_child_row() {
|
||||||
|
local type_key="${1:-}" subnet="${2:-}" tunnel="${3:-}" desc="${4:-}"
|
||||||
|
local desc_part=""
|
||||||
|
[[ -n "$desc" ]] && desc_part=" $desc"
|
||||||
|
printf " · %-10s %-18s \033[2mtunnel:\033[0m %-8s%s\n" \
|
||||||
|
"$type_key" "$subnet" "$tunnel" "$desc_part"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::subnet::show_peers() {
|
||||||
|
local peers_csv="${1:-}"
|
||||||
|
[[ -z "$peers_csv" ]] && return 0
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
local peers_arr=()
|
||||||
|
IFS=',' read -ra peers_arr <<< "$peers_csv"
|
||||||
|
local count="${#peers_arr[@]}"
|
||||||
|
|
||||||
|
printf " Peers (%s):\n" "$count"
|
||||||
|
|
||||||
|
# Group peers by identity, then by subnet child (for groups)
|
||||||
|
# Build: identity -> list of peer names
|
||||||
|
declare -A identity_peers_map=()
|
||||||
|
local no_identity=()
|
||||||
|
|
||||||
|
for peer in "${peers_arr[@]}"; do
|
||||||
|
peer="${peer// /}"
|
||||||
|
[[ -z "$peer" ]] && continue
|
||||||
|
local id_name
|
||||||
|
id_name=$(identity::get_name "$peer")
|
||||||
|
if [[ -n "$id_name" ]]; then
|
||||||
|
identity_peers_map["$id_name"]+="${peer},"
|
||||||
|
else
|
||||||
|
no_identity+=("$peer")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Print identity groups on same line
|
||||||
|
for id_name in "${!identity_peers_map[@]}"; do
|
||||||
|
local peer_list="${identity_peers_map[$id_name]%,}"
|
||||||
|
printf " · %s\n" "${peer_list//,/, }"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Print peers without identity one per line
|
||||||
|
for peer in "${no_identity[@]}"; do
|
||||||
|
printf " · %s\n" "$peer"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::subnet::show_peers_annotated() {
|
||||||
|
# For group subnets — peers annotated with their subnet child
|
||||||
|
local peers_csv="${1:-}" subnets_file="${2:-}"
|
||||||
|
[[ -z "$peers_csv" ]] && return 0
|
||||||
|
|
||||||
|
local peers_arr=()
|
||||||
|
IFS=',' read -ra peers_arr <<< "$peers_csv"
|
||||||
|
local count="${#peers_arr[@]}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf " Peers (%s):\n" "$count"
|
||||||
|
|
||||||
|
declare -A identity_peers_map=() # identity -> "peer→child,peer→child"
|
||||||
|
local no_identity=()
|
||||||
|
|
||||||
|
for peer in "${peers_arr[@]}"; do
|
||||||
|
peer="${peer// /}"
|
||||||
|
[[ -z "$peer" ]] && continue
|
||||||
|
|
||||||
|
# Find which child subnet this peer's IP belongs to
|
||||||
|
local peer_ip child_name=""
|
||||||
|
peer_ip=$(peers::get_ip "$peer" 2>/dev/null) || peer_ip=""
|
||||||
|
if [[ -n "$peer_ip" ]]; then
|
||||||
|
local result
|
||||||
|
result=$(json::subnet_for_ip "$subnets_file" "$peer_ip" 2>/dev/null) || true
|
||||||
|
[[ -n "$result" ]] && child_name="${result##*|}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local annotated="${peer}"
|
||||||
|
[[ -n "$child_name" ]] && annotated="${peer} \033[2m→ ${child_name}\033[0m"
|
||||||
|
|
||||||
|
local id_name
|
||||||
|
id_name=$(identity::get_name "$peer")
|
||||||
|
if [[ -n "$id_name" ]]; then
|
||||||
|
identity_peers_map["$id_name"]+="${annotated},"
|
||||||
|
else
|
||||||
|
no_identity+=("$annotated")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for id_name in "${!identity_peers_map[@]}"; do
|
||||||
|
local peer_list="${identity_peers_map[$id_name]%,}"
|
||||||
|
printf " · %b\n" "${peer_list//,/, }"
|
||||||
|
done
|
||||||
|
|
||||||
|
for peer in "${no_identity[@]}"; do
|
||||||
|
printf " · %b\n" "$peer"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
2
wgctl
2
wgctl
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
source "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/core.sh"
|
source "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/core.sh"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue