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
|
||||
|
||||
local identity_rule
|
||||
identity_rule=$(identity::rule "$identity_name") || true
|
||||
local rules
|
||||
rules=$(identity::rules "$identity_name")
|
||||
|
||||
[[ -z "$identity_rule" ]] && {
|
||||
# No identity rule — warn if no peer rule either
|
||||
if [[ -z "$rules" ]]; then
|
||||
# No identity rules — warn if no peer rule either
|
||||
if [[ -z "$peer_rule" ]]; then
|
||||
policy::warn_no_rule "$full_name"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
fi
|
||||
|
||||
# Apply identity rule
|
||||
rule::exists "$identity_rule" && rule::apply "$identity_rule" "$ip" "$full_name" || true
|
||||
# Apply all identity rules
|
||||
rule::_apply_identity_rule "$full_name" "$ip"
|
||||
|
||||
# Warn based on strict_rule
|
||||
if policy::strict_rule "$effective_policy"; then
|
||||
policy::warn_strict_rule "$identity_name" "$effective_policy" "$identity_rule"
|
||||
elif [[ -n "$peer_rule" && "$peer_rule" != "$identity_rule" ]]; then
|
||||
policy::warn_additive_rule "$identity_name" "$identity_rule" "$peer_rule"
|
||||
local strict
|
||||
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
||||
if [[ "$strict" == "true" ]]; then
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,10 +107,13 @@ function cmd::identity::_list() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
ui::identity::header
|
||||
while IFS='|' read -r name peer_count types; do
|
||||
ui::identity::row "$name" "$peer_count" "$types"
|
||||
echo ""
|
||||
while IFS='|' read -r name peer_count types rules policy; do
|
||||
local rules_display
|
||||
rules_display=$(echo "$rules" | sed 's/,/, /g')
|
||||
ui::identity::list_row_compact "$name" "$peer_count" "$rules_display" "$policy"
|
||||
done <<< "$data"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function cmd::identity::_show() {
|
||||
|
|
@ -126,19 +129,40 @@ function cmd::identity::_show() {
|
|||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||
identity::require_exists "$name" || return 1
|
||||
|
||||
local data peer_count="0"
|
||||
data=$(identity::show_data "$name")
|
||||
# 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")
|
||||
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
|
||||
case "$key" in
|
||||
name)
|
||||
peer_count=$(echo "$data" | grep '^peer_count|' | cut -d'|' -f2)
|
||||
ui::identity::detail_name "$val" "$peer_count"
|
||||
;;
|
||||
peer_count) ;; # consumed above
|
||||
name|peer_count) ;;
|
||||
device)
|
||||
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"
|
||||
;;
|
||||
esac
|
||||
|
|
@ -147,25 +171,27 @@ function cmd::identity::_show() {
|
|||
echo ""
|
||||
}
|
||||
|
||||
|
||||
function cmd::identity::_device_status() {
|
||||
local peer_name="${1:-}"
|
||||
local -n _handshakes="${2:-__empty_map}"
|
||||
|
||||
local peer_ip
|
||||
peer_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || return 0
|
||||
[[ -z "$peer_ip" ]] && return 0
|
||||
|
||||
local is_blocked is_restricted pubkey handshake_ts
|
||||
is_blocked=$(peers::is_blocked "$peer_name")
|
||||
is_restricted=$(peers::is_restricted "$peer_name")
|
||||
pubkey=$(cat "$(ctx::clients)/${peer_name}.pub" 2>/dev/null) || pubkey=""
|
||||
handshake_ts=$(wg show wg0 latest-handshakes 2>/dev/null \
|
||||
| awk -v pk="$pubkey" '$1==pk{print $2}') || handshake_ts=0
|
||||
peers::is_blocked "$peer_name" && is_blocked="true" || is_blocked="false"
|
||||
peers::is_restricted "$peer_name" && is_restricted="true" || is_restricted="false"
|
||||
|
||||
local last_ts last_evt
|
||||
pubkey="$(keys::public "$peer_name")"
|
||||
handshake_ts="${_handshakes[$pubkey]:-0}"
|
||||
|
||||
local 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
|
||||
status=$(peers::format_status \
|
||||
status=$(peers::format_status_verbose \
|
||||
"$peer_name" "$pubkey" "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||
echo " — ${status}"
|
||||
}
|
||||
|
|
@ -338,10 +364,17 @@ function cmd::identity::_rule_assign() {
|
|||
identity::require_exists "$name" || return 1
|
||||
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
|
||||
|
||||
identity::set_rule "$name" "$rule"
|
||||
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
|
||||
|
||||
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
|
||||
auto=$(identity::rule_flags "$name" "auto_apply")
|
||||
if [[ "$auto" != "false" ]]; then
|
||||
|
|
@ -350,17 +383,19 @@ function cmd::identity::_rule_assign() {
|
|||
log::ok "Rules reapplied"
|
||||
fi
|
||||
|
||||
# Warn about strict_rule impact
|
||||
# Warn about strict_rule
|
||||
if policy::strict_rule "$(identity::policy "$name")"; then
|
||||
log::warn "Strict rule is enabled for identity '${name}' — peer rules will not be additive"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::identity::_rule_unassign() {
|
||||
local name=""
|
||||
local name="" rule="" all=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--rule) rule="$2"; shift 2 ;;
|
||||
--all) all=true; shift ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -368,16 +403,46 @@ function cmd::identity::_rule_unassign() {
|
|||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||
identity::require_exists "$name" || return 1
|
||||
|
||||
local current_rule
|
||||
current_rule=$(identity::rule "$name")
|
||||
if [[ -z "$current_rule" ]]; then
|
||||
log::warn "Identity '${name}' has no rule assigned"
|
||||
if $all; then
|
||||
local rules
|
||||
rules=$(identity::rules "$name")
|
||||
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
|
||||
fi
|
||||
|
||||
identity::clear_rule "$name"
|
||||
log::ok "Rule removed from identity '${name}'"
|
||||
log::info "Note: existing fw rules from '${current_rule}' are not automatically removed — run 'wgctl rule reapply' if needed"
|
||||
[[ -z "$rule" ]] && {
|
||||
log::error "Missing required flag: --rule (or use --all to remove all)"
|
||||
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() {
|
||||
|
|
@ -392,20 +457,28 @@ function cmd::identity::_rule_show() {
|
|||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||
identity::require_exists "$name" || return 1
|
||||
|
||||
local rule policy
|
||||
rule=$(identity::rule "$name")
|
||||
local rules policy strict auto
|
||||
rules=$(identity::rules "$name")
|
||||
policy=$(identity::policy "$name")
|
||||
|
||||
local strict auto
|
||||
strict=$(identity::rule_flags "$name" "strict_rule")
|
||||
auto=$(identity::rule_flags "$name" "auto_apply")
|
||||
|
||||
echo ""
|
||||
ui::row "Identity" "$name"
|
||||
ui::row "Rule" "${rule:-—}"
|
||||
ui::row "Policy" "$policy"
|
||||
ui::row "Strict rule" "$( [[ "$strict" == "true" ]] && echo "yes" || echo "no" )"
|
||||
ui::row "Auto apply" "$( [[ "$auto" != "false" ]] && echo "yes" || echo "no" )"
|
||||
ui::row "Strict rule" "$(ui::bool "$strict")"
|
||||
ui::row "Auto apply" "$(ui::bool "$auto")"
|
||||
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 ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,9 +89,9 @@ function cmd::inspect::_peer_info() {
|
|||
local activity_current
|
||||
activity_current=$(peers::format_activity_current "$public_key")
|
||||
|
||||
local rule_file=""
|
||||
local rule_extends=""
|
||||
if [[ -n "$rule" ]]; then
|
||||
local rule_file
|
||||
rule_file="$(rule::path "$rule" 2>/dev/null)" || true
|
||||
if [[ -n "$rule_file" ]]; then
|
||||
local ext=()
|
||||
|
|
@ -127,22 +127,81 @@ function cmd::inspect::_peer_info() {
|
|||
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() {
|
||||
local name="${1:-}"
|
||||
local rule
|
||||
rule=$(peers::get_meta "$name" "rule")
|
||||
[[ -z "$rule" ]] && return 0
|
||||
|
||||
local identity_name identity_rules strict
|
||||
identity_name=$(identity::get_name "$name")
|
||||
if [[ -n "$identity_name" ]]; then
|
||||
identity_rules=$(identity::rules "$identity_name")
|
||||
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
||||
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
|
||||
|
||||
cmd::inspect::_section "Rule: ${rule}"
|
||||
|
||||
if rule::render_extends_tree "$rule"; then
|
||||
# printf "\n"
|
||||
: # no-op
|
||||
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
|
||||
# No inheritance — flat view
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
# ============================================
|
||||
|
||||
function cmd::list::on_load() {
|
||||
load_module identity
|
||||
load_module ui
|
||||
|
||||
flag::register --type
|
||||
flag::register --rule
|
||||
flag::register --group
|
||||
|
|
@ -29,87 +32,29 @@ Usage: wgctl list [options]
|
|||
List all WireGuard clients.
|
||||
|
||||
Options:
|
||||
--type <type> Filter by device type (desktop, laptop, phone, tablet)
|
||||
--type <type> Filter by device type
|
||||
--rule <rule> Filter by assigned rule
|
||||
--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
|
||||
--offline Show only disconnected clients
|
||||
--blocked Show only fully blocked clients (removed from WireGuard)
|
||||
--restricted Show only restricted clients (specific IP/port blocks applied)
|
||||
--blocked Show only fully blocked clients
|
||||
--restricted Show only restricted 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
|
||||
|
||||
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:
|
||||
wgctl list
|
||||
wgctl list --type phone
|
||||
wgctl list --rule user
|
||||
wgctl list --group family
|
||||
wgctl list --identity nuno
|
||||
wgctl list --online
|
||||
wgctl list --blocked
|
||||
wgctl list --restricted
|
||||
wgctl list --detailed
|
||||
wgctl list --name phone-nuno
|
||||
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
|
||||
# ============================================
|
||||
|
|
@ -133,7 +78,6 @@ function cmd::list::show_client() {
|
|||
local public_key
|
||||
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
|
||||
type=$(peers::get_meta "$name" "type" 2>/dev/null)
|
||||
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
||||
|
|
@ -241,36 +185,53 @@ function cmd::list::run() {
|
|||
|
||||
cmd::list::_precompute_all
|
||||
|
||||
if $detailed; then
|
||||
# Resolve 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
|
||||
|
||||
log::section "WireGuard Clients"
|
||||
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
|
||||
|
||||
# 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
|
||||
fi
|
||||
|
||||
local filter_desc=""
|
||||
cmd::list::_build_filter_desc
|
||||
|
||||
declare -A rule_counts=() group_counts=()
|
||||
_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}}"
|
||||
if $detailed; then
|
||||
cmd::list::_render_detailed "$collected_rows"
|
||||
cmd::list::_render_summary_from_rows "$collected_rows"
|
||||
return 0
|
||||
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() {
|
||||
local filter_type="$1" callback="$2"
|
||||
function cmd::list::_collect_all_rows() {
|
||||
# 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
|
||||
dir="$(ctx::clients)"
|
||||
|
||||
|
|
@ -278,8 +239,124 @@ function cmd::list::_iter_confs() {
|
|||
[[ -f "$conf" ]] || continue
|
||||
local client_name
|
||||
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 && \
|
||||
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
||||
continue
|
||||
|
|
@ -288,16 +365,111 @@ function cmd::list::_iter_confs() {
|
|||
local ip="${p_ips[$client_name]:-}"
|
||||
[[ -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}"
|
||||
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||
|
||||
"$callback" "$client_name" "$ip" "$type"
|
||||
cmd::list::_render_row "$client_name" "$ip" "$type"
|
||||
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() {
|
||||
|
|
@ -321,7 +493,7 @@ function cmd::list::_render_row() {
|
|||
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
||||
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" \
|
||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||
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 [[ "${_list_header_printed:-false}" == "false" ]]; then
|
||||
log::section "WireGuard Clients"
|
||||
cmd::list::_render_header $has_groups
|
||||
_list_header_printed=true
|
||||
fi
|
||||
|
|
@ -344,26 +515,12 @@ function cmd::list::_render_row() {
|
|||
|
||||
if $has_groups; then
|
||||
local main_group="${p_main_groups[$client_name]:-}"
|
||||
if [[ -n "$main_group" ]]; then
|
||||
group_display="$main_group"
|
||||
else
|
||||
group_display="${peer_group_map[$client_name]:-—}"
|
||||
fi
|
||||
|
||||
if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
|
||||
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" \
|
||||
local group_display="${main_group:-${peer_group_map[$client_name]:-—}}"
|
||||
printf " %-28s %-15s %-13s %-12s %-12s %s %s\n" \
|
||||
"$client_name" "$ip" "$display_type" "$rule" \
|
||||
"$group_display" "$padded_status" "$last_seen"
|
||||
else
|
||||
local rule_col_width=12
|
||||
[[ "$rule" == "—" ]] && rule_col_width=14
|
||||
printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \
|
||||
printf " %-28s %-15s %-13s %-12s %s %s\n" \
|
||||
"$client_name" "$ip" "$display_type" "$rule" \
|
||||
"$padded_status" "$last_seen"
|
||||
fi
|
||||
|
|
@ -374,25 +531,22 @@ function cmd::list::_render_row() {
|
|||
# ============================================
|
||||
|
||||
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=()
|
||||
while IFS="|" read -r name ip rule type last_ts last_evt main_group; do
|
||||
[[ -z "$name" ]] && continue
|
||||
p_ips["$name"]="$ip"
|
||||
p_rules["$name"]="${rule:-—}"
|
||||
p_rules["$name"]="${rule:-}"
|
||||
p_types["$name"]="${type:-}"
|
||||
p_last_ts["$name"]="$last_ts"
|
||||
p_last_evt["$name"]="$last_evt"
|
||||
p_main_groups["$name"]="${main_group:-}"
|
||||
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
||||
|
||||
# Fill type from IP for peers missing meta type (pre-migration peers)
|
||||
for name in "${!p_ips[@]}"; do
|
||||
[[ -n "${p_types[$name]:-}" ]] && continue
|
||||
p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
|
||||
done
|
||||
|
||||
# WireGuard handshakes + endpoints
|
||||
declare -gA wg_handshakes=() wg_endpoints=()
|
||||
while IFS=$'\t' read -r pubkey ts; do
|
||||
[[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
||||
|
|
@ -401,11 +555,9 @@ function cmd::list::_precompute_all() {
|
|||
[[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
||||
done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
||||
|
||||
# Block/restricted status
|
||||
declare -gA p_blocked=() p_restricted=()
|
||||
cmd::list::_precompute_block_status p_blocked p_restricted
|
||||
|
||||
# Public keys
|
||||
declare -gA p_pubkeys=()
|
||||
local dir
|
||||
dir="$(ctx::clients)"
|
||||
|
|
@ -416,7 +568,6 @@ function cmd::list::_precompute_all() {
|
|||
p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
||||
done
|
||||
|
||||
# Groups + main group
|
||||
has_groups=false
|
||||
declare -gA peer_group_map=()
|
||||
local groups_dir
|
||||
|
|
@ -429,27 +580,8 @@ function cmd::list::_precompute_all() {
|
|||
done < <(json::peer_group_map "$groups_dir")
|
||||
fi
|
||||
|
||||
# Resolve identity filter into a peer set
|
||||
# Identity precompute (for --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() {
|
||||
|
|
@ -477,19 +609,15 @@ function cmd::list::_precompute_block_status() {
|
|||
}
|
||||
|
||||
# ============================================
|
||||
# Filter helpers
|
||||
# Header / Footer (table layout)
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_build_filter_desc() {
|
||||
filter_desc=""
|
||||
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
||||
[[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
|
||||
[[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
|
||||
[[ -n "$filter_identity" ]] && filter_desc+="identity=${filter_identity} "
|
||||
$online_only && filter_desc+="online "
|
||||
$offline_only && filter_desc+="offline "
|
||||
$blocked_only && filter_desc+="blocked "
|
||||
filter_desc="${filter_desc% }"
|
||||
function cmd::list::_render_header() {
|
||||
ui::peer::list_header_table "$1"
|
||||
}
|
||||
|
||||
function cmd::list::_render_footer() {
|
||||
ui::peer::list_footer_table "$1"
|
||||
}
|
||||
|
||||
function cmd::list::_build_group_summary() {
|
||||
|
|
|
|||
|
|
@ -106,18 +106,11 @@ function cmd::policy::_list() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
printf " %-14s %-8s %-14s %-12s %-12s %s\n" \
|
||||
"NAME" "TUNNEL" "DEFAULT RULE" "STRICT RULE" "AUTO APPLY" "DESCRIPTION"
|
||||
ui::divider 84
|
||||
|
||||
echo ""
|
||||
while IFS='|' read -r name tunnel default_rule strict auto desc; do
|
||||
local rule_display="${default_rule:-—}"
|
||||
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"
|
||||
ui::policy::list_row "$name" "$default_rule" "$strict" "$auto"
|
||||
done <<< "$data"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function cmd::policy::_show() {
|
||||
|
|
@ -133,14 +126,30 @@ function cmd::policy::_show() {
|
|||
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
||||
policy::require_exists "$name" || return 1
|
||||
|
||||
echo ""
|
||||
ui::row "Name" "$name"
|
||||
ui::row "Tunnel mode" "$(policy::tunnel_mode "$name")"
|
||||
local dr
|
||||
local dr tunnel strict auto
|
||||
dr=$(policy::default_rule "$name")
|
||||
ui::row "Default rule" "${dr:-—}"
|
||||
ui::row "Strict rule" "$(policy::strict_rule "$name" && echo "yes" || echo "no")"
|
||||
ui::row "Auto apply" "$(policy::auto_apply "$name" && echo "yes" || echo "no")"
|
||||
tunnel=$(policy::tunnel_mode "$name")
|
||||
strict=$(policy::strict_rule "$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 ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -358,13 +358,15 @@ function cmd::rule::show() {
|
|||
ui::row "Group" "${group:-—}"
|
||||
ui::row "DNS" "$dns_display"
|
||||
|
||||
printf "\n"
|
||||
# ── Extends + own rules ────────────────────────
|
||||
if rule::render_extends_tree "$name"; then
|
||||
# Has inheritance — tree already rendered
|
||||
printf "\n"
|
||||
:
|
||||
else
|
||||
# No inheritance — flat view
|
||||
rule::render_flat "$name"
|
||||
printf "\n"
|
||||
fi
|
||||
|
||||
# ── Resolved ──────────────────────────────────
|
||||
|
|
@ -381,7 +383,6 @@ function cmd::rule::show() {
|
|||
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \
|
||||
<<< "$res_block_ips"$'\n'"$res_block_ports"
|
||||
printf "\n"
|
||||
|
||||
fi
|
||||
|
||||
# ── Peers ─────────────────────────────────────
|
||||
|
|
@ -390,6 +391,9 @@ function cmd::rule::show() {
|
|||
|
||||
local peer_count=${#peer_list[@]}
|
||||
|
||||
ui::empty "$peer_count" && return 0
|
||||
|
||||
printf "\n"
|
||||
printf "\033[0;37m── Peers (%s) \033[0m%s\n\n" \
|
||||
"$(color::gray "${peer_count}")" \
|
||||
"$(printf '\033[0;37m─%.0s' {1..35})"
|
||||
|
|
|
|||
|
|
@ -92,14 +92,25 @@ function cmd::subnet::_list() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
ui::subnet::header
|
||||
|
||||
echo ""
|
||||
local prev_group=""
|
||||
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"
|
||||
prev_group=$(cmd::subnet::_update_prev_group "$is_group" "$group_parent" "$prev_group")
|
||||
ui::subnet::row "$display_name" "$subnet" "$type_key" "$tunnel_mode" "$desc" "$is_group"
|
||||
if [[ "$is_group" == "true" ]]; then
|
||||
# Print group parent header when we encounter first child
|
||||
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"
|
||||
echo ""
|
||||
}
|
||||
|
||||
function cmd::subnet::_maybe_group_separator() {
|
||||
|
|
@ -137,34 +148,45 @@ function cmd::subnet::_show() {
|
|||
data=$(subnet::show_data "$name")
|
||||
|
||||
local is_group="false"
|
||||
local show_name="" show_subnet="" show_tunnel="" show_desc=""
|
||||
|
||||
while IFS='|' read -r key val rest; do
|
||||
case "$key" in
|
||||
name)
|
||||
ui::subnet::detail "$val" "$is_group"
|
||||
;;
|
||||
is_group)
|
||||
is_group="$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)
|
||||
name) show_name="$val" ;;
|
||||
is_group) is_group="$val" ;;
|
||||
subnet) show_subnet="$val" ;;
|
||||
tunnel_mode) show_tunnel="$val" ;;
|
||||
desc) show_desc="$val" ;;
|
||||
esac
|
||||
done <<< "$data"
|
||||
|
||||
if [[ "$is_group" == "true" ]]; then
|
||||
# Group display
|
||||
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::child_row "$c_type" "$c_subnet" "$c_tunnel" "$c_desc"
|
||||
;;
|
||||
esac
|
||||
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::peers_in_use "$peers_using"
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -136,12 +136,12 @@ function cmd::test::run_all_integration_sections() {
|
|||
|
||||
function cmd::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 --offline" "" list --offline
|
||||
cmd::test::run_cmd "list --blocked" "" list --blocked
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -225,8 +225,8 @@ function cmd::test::section_subnet() {
|
|||
"$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 show desktop" "Subnet" subnet show --name desktop
|
||||
cmd::test::run_cmd "subnet show guests group" "group" subnet show --name guests
|
||||
cmd::test::run_cmd "subnet show desktop" "tunnel:" subnet show --name desktop
|
||||
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 "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
|
||||
}
|
||||
|
||||
# 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() {
|
||||
local format="$1"
|
||||
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_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
|
||||
function json::policy_get() { python3 "$JSON_HELPER" policy_get "$@" </dev/null; }
|
||||
function json::policy_list() { python3 "$JSON_HELPER" policy_list "$@" </dev/null; }
|
||||
|
|
|
|||
|
|
@ -1764,6 +1764,79 @@ def subnet_exists(file, name):
|
|||
# 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):
|
||||
"""Read an identity file, return dict or None"""
|
||||
try:
|
||||
|
|
@ -1822,7 +1895,10 @@ def _parse_peer_name(peer_name):
|
|||
return (peer_type, identity, index)
|
||||
|
||||
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
|
||||
for id_file in sorted(glob.glob(f"{identities_dir}/*.identity")):
|
||||
try:
|
||||
|
|
@ -1831,8 +1907,15 @@ def identity_list(identities_dir):
|
|||
name = data.get('name', '')
|
||||
peers = data.get('peers', [])
|
||||
devices = data.get('devices', {})
|
||||
types = sorted(set(d.get('type', '') for d in devices.values() if d.get('type')))
|
||||
print(f"{name}|{len(peers)}|{','.join(types)}")
|
||||
rules = data.get('rules', [])
|
||||
# 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:
|
||||
continue
|
||||
|
||||
|
|
@ -2426,6 +2509,11 @@ commands = {
|
|||
'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:]),
|
||||
'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__':
|
||||
|
|
|
|||
58
core/ui.sh
58
core/ui.sh
|
|
@ -77,6 +77,48 @@ function ui::center() {
|
|||
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() {
|
||||
local rule="$1"
|
||||
if [[ "$rule" =~ ACCEPT|DNAT ]]; then
|
||||
|
|
@ -109,9 +151,19 @@ function ui::skip_if_empty() {
|
|||
}
|
||||
|
||||
function ui::empty() {
|
||||
# ui::empty "$var" && return 0
|
||||
# ui::empty "${array[*]}" && return 0
|
||||
[[ -z "${1// }" ]]
|
||||
local val="${1:-}"
|
||||
# Empty string or whitespace only
|
||||
[[ -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",
|
||||
"laptop-nuno": "94.63.0.129",
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
# identity.module.sh — identity file management and peer-name inference
|
||||
|
||||
declare -gA __empty_map=()
|
||||
|
||||
# ===========================================================================
|
||||
# Path helpers
|
||||
# ===========================================================================
|
||||
|
|
@ -255,38 +257,6 @@ function identity::rename_peer() {
|
|||
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>
|
||||
# Returns the policy name assigned to an identity, or "default".
|
||||
|
|
@ -374,3 +344,79 @@ function identity::reapply_rules() {
|
|||
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
|
||||
[[ -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"
|
||||
}
|
||||
|
|
@ -99,32 +99,21 @@ function rule::is_applied() {
|
|||
# Rule Application
|
||||
# ============================================
|
||||
|
||||
function rule::apply() {
|
||||
local rule_name="${1:?rule_name required}"
|
||||
local client_ip="${2:?client_ip required}"
|
||||
local peer_name="${3:-}"
|
||||
function rule::_apply_entries() {
|
||||
local rule_name="${1:?}" client_ip="${2:?}"
|
||||
|
||||
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
|
||||
log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
|
||||
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
||||
log::debug "Rule '${rule_name}' already applied to: ${client_ip}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Process block_ips
|
||||
while IFS= read -r block_ip; do
|
||||
[[ -z "$block_ip" ]] && continue
|
||||
fw::block_ip "$client_ip" "$block_ip"
|
||||
done < <(rule::get "$rule_name" "block_ips")
|
||||
|
||||
# Process block_ports
|
||||
while IFS= read -r entry; do
|
||||
[[ -z "$entry" ]] && continue
|
||||
local target port proto
|
||||
|
|
@ -133,13 +122,11 @@ function rule::apply() {
|
|||
fw::block_port "$client_ip" "$target" "$port" "$proto"
|
||||
done < <(rule::get "$rule_name" "block_ports")
|
||||
|
||||
# Process allow_ips (inserted before blocks)
|
||||
while IFS= read -r allow_ip; do
|
||||
[[ -z "$allow_ip" ]] && continue
|
||||
fw::allow_ip "$client_ip" "$allow_ip"
|
||||
done < <(rule::get "$rule_name" "allow_ips")
|
||||
|
||||
# Process allow_ports (highest priority)
|
||||
while IFS= read -r entry; do
|
||||
[[ -z "$entry" ]] && continue
|
||||
local target port proto
|
||||
|
|
@ -148,27 +135,46 @@ function rule::apply() {
|
|||
fw::allow_port "$client_ip" "$target" "$port" "$proto"
|
||||
done < <(rule::get "$rule_name" "allow_ports")
|
||||
|
||||
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
||||
|
||||
# DNS redirect
|
||||
local dns_redirect
|
||||
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
|
||||
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)
|
||||
if ! fw::_nat_exists -i wg0 -s "${peer_subnet}.0/24" \
|
||||
-p udp --dport 53 -j DNAT \
|
||||
--to-destination "$(config::dns):53" 2>/dev/null; then
|
||||
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
|
||||
|
||||
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() {
|
||||
local rule_name="${1:-}" client_ip="${2:-}"
|
||||
|
||||
|
|
@ -236,21 +242,31 @@ function rule::unapply() {
|
|||
|
||||
function rule::_apply_identity_rule() {
|
||||
local peer_name="${1:-}" client_ip="${2:-}"
|
||||
|
||||
local identity_name
|
||||
identity_name=$(identity::get_name "$peer_name")
|
||||
[[ -z "$identity_name" ]] && return 0
|
||||
|
||||
local identity_rule strict
|
||||
identity_rule=$(identity::rule "$identity_name")
|
||||
[[ -z "$identity_rule" ]] && return 0
|
||||
local rules
|
||||
rules=$(identity::rules "$identity_name")
|
||||
[[ -z "$rules" ]] && return 0
|
||||
|
||||
local strict
|
||||
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
||||
|
||||
if [[ "$strict" == "true" ]]; then
|
||||
# Strict: flush and apply only identity rules — peer rule ignored
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -266,30 +282,28 @@ function rule::full_restore_peer() {
|
|||
|
||||
local peer_rule
|
||||
peer_rule=$(peers::get_meta "$peer_name" "rule")
|
||||
[[ -n "$peer_rule" ]] && rule::apply "$peer_rule" "$client_ip" "$peer_name"
|
||||
|
||||
local strict
|
||||
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"
|
||||
}
|
||||
|
||||
function rule::reapply_all() {
|
||||
local rule_name="${1:-}"
|
||||
rule::require_exists "$rule_name" || return 1
|
||||
|
||||
local peers=()
|
||||
mapfile -t peers < <(peers::with_rule "$rule_name")
|
||||
[[ ${#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::_get_identity_strict() {
|
||||
local peer_name="${1:-}"
|
||||
local identity_name
|
||||
identity_name=$(identity::get_name "$peer_name")
|
||||
[[ -z "$identity_name" ]] && echo "false" && return 0
|
||||
identity::rule_flags "$identity_name" "strict_rule"
|
||||
}
|
||||
|
||||
function rule::restore_all() {
|
||||
|
|
@ -320,125 +334,19 @@ function rule::restore_all() {
|
|||
# ============================================
|
||||
|
||||
function rule::render_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"
|
||||
|
||||
return 0
|
||||
ui::rule::flat "$1"
|
||||
}
|
||||
|
||||
function rule::render_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"
|
||||
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"
|
||||
ui::rule::entries "$1"
|
||||
}
|
||||
|
||||
function rule::render_own_entries() {
|
||||
local rule_name="${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
|
||||
ui::rule::own_entries "$1"
|
||||
}
|
||||
|
||||
function rule::render_extends_tree() {
|
||||
local rule_name="${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
|
||||
ui::rule::tree "$1"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -53,3 +53,29 @@ function ui::identity::migrate_summary() {
|
|||
log::ok "Created identity entries for ${created} peers (${skipped} skipped)"
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
local display_name="${1:-}" subnet="${2:-}" type_key="${3:-}" \
|
||||
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"
|
||||
}
|
||||
|
||||
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() {
|
||||
echo ""
|
||||
}
|
||||
|
|
@ -49,3 +97,117 @@ function ui::subnet::peers_in_use() {
|
|||
echo ""
|
||||
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
|
||||
set -Eeuo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
source "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/core.sh"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue