feat: main group display, group::has_peer, config validation, full block cleanup on unblock, ui::empty helper, blocks header count
This commit is contained in:
parent
87f6c770ef
commit
7323bf20f1
14 changed files with 536 additions and 197 deletions
|
|
@ -94,7 +94,7 @@ function cmd::block::run() {
|
||||||
# Full block if no specific targets
|
# Full block if no specific targets
|
||||||
if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \
|
if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \
|
||||||
${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
|
${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
|
||||||
if peers::is_blocked "$name" || block::has_file "$name"; then
|
if peers::is_blocked "$name"; then
|
||||||
log::wg_warning "Client is already blocked: ${name}"
|
log::wg_warning "Client is already blocked: ${name}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ function cmd::group::on_load() {
|
||||||
flag::register --type
|
flag::register --type
|
||||||
flag::register --rule
|
flag::register --rule
|
||||||
flag::register --new-name
|
flag::register --new-name
|
||||||
|
flag::register --main
|
||||||
flag::register --force
|
flag::register --force
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +84,7 @@ function cmd::group::run() {
|
||||||
rename) cmd::group::rename "$@" ;;
|
rename) cmd::group::rename "$@" ;;
|
||||||
peer) cmd::group::peer "$@" ;;
|
peer) cmd::group::peer "$@" ;;
|
||||||
rm-peers) cmd::group::rm_peers "$@" ;;
|
rm-peers) cmd::group::rm_peers "$@" ;;
|
||||||
|
set-main) cmd::group::set_main "$@" ;;
|
||||||
block) cmd::group::block "$@" ;;
|
block) cmd::group::block "$@" ;;
|
||||||
unblock) cmd::group::unblock "$@" ;;
|
unblock) cmd::group::unblock "$@" ;;
|
||||||
rule) cmd::group::rule "$@" ;;
|
rule) cmd::group::rule "$@" ;;
|
||||||
|
|
@ -343,13 +345,14 @@ function cmd::group::peer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::group::peer_add() {
|
function cmd::group::peer_add() {
|
||||||
local name="" peer="" type=""
|
local name="" peer="" type="" set_main=false
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
|
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
|
||||||
--peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;;
|
--peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;;
|
||||||
--type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;;
|
--type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;;
|
||||||
|
--main) util::require_flag "--main" "${2:-}" || return 1; set_main=true; shift ;;
|
||||||
--help) cmd::group::help; return ;;
|
--help) cmd::group::help; return ;;
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -369,6 +372,11 @@ function cmd::group::peer_add() {
|
||||||
|
|
||||||
group::add_peer "$name" "$peer"
|
group::add_peer "$name" "$peer"
|
||||||
log::wg_success "Added '${peer}' to group '${name}'"
|
log::wg_success "Added '${peer}' to group '${name}'"
|
||||||
|
|
||||||
|
if $set_main; then
|
||||||
|
peers::set_main_group "$peer_name" "$group_name"
|
||||||
|
log::wg_success "Set '${group_name}' as main group for ${peer_name}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::group::peer_remove() {
|
function cmd::group::peer_remove() {
|
||||||
|
|
@ -393,6 +401,34 @@ function cmd::group::peer_remove() {
|
||||||
log::wg_success "Removed '${peer}' from group '${name}'"
|
log::wg_success "Removed '${peer}' from group '${name}'"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cmd::group::set_main() {
|
||||||
|
local group_name="" peer_name="" type=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--name) group_name="$2"; shift 2 ;;
|
||||||
|
--peer) peer_name="$2"; shift 2 ;;
|
||||||
|
--type) type="$2"; shift 2 ;;
|
||||||
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$group_name" ]] && log::error "Missing --name" && return 1
|
||||||
|
[[ -z "$peer_name" ]] && log::error "Missing --peer" && return 1
|
||||||
|
|
||||||
|
# Resolve peer name
|
||||||
|
peer_name=$(peers::resolve_and_require "$peer_name" "$type") || return 1
|
||||||
|
|
||||||
|
# Verify peer is in the group
|
||||||
|
if ! group::has_peer "$group_name" "$peer_name"; then
|
||||||
|
log::error "Peer '${peer_name}' is not in group '${group_name}'"
|
||||||
|
log::info "Add them first: wgctl group peer add --name ${group_name} --peer ${peer_name}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
peers::set_main_group "$peer_name" "$group_name"
|
||||||
|
log::wg_success "Main group for '${peer_name}' set to '${group_name}'"
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Remove peers from WireGuard
|
# Remove peers from WireGuard
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -444,56 +480,6 @@ function cmd::group::_rm_peer_cb() {
|
||||||
cmd::remove::run --name "$peer_name" --force
|
cmd::remove::run --name "$peer_name" --force
|
||||||
}
|
}
|
||||||
|
|
||||||
# function cmd::group::rm_peers() {
|
|
||||||
# local name="" force=false
|
|
||||||
|
|
||||||
# while [[ $# -gt 0 ]]; do
|
|
||||||
# case "$1" in
|
|
||||||
# --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
|
|
||||||
# --force) force=true; shift ;;
|
|
||||||
# --help) cmd::group::help; return ;;
|
|
||||||
# *) log::error "Unknown flag: $1"; return 1 ;;
|
|
||||||
# esac
|
|
||||||
# done
|
|
||||||
|
|
||||||
# [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
|
||||||
# group::require_exists "$name" || return 1
|
|
||||||
|
|
||||||
# local peers_list=()
|
|
||||||
# mapfile -t peers_list < <(group::peers "$name")
|
|
||||||
# local peer_count=${#peers_list[@]}
|
|
||||||
# [[ -z "${peers_list[0]}" ]] && peer_count=0
|
|
||||||
|
|
||||||
# if [[ "$peer_count" -eq 0 ]]; then
|
|
||||||
# log::wg_warning "Group '${name}' has no peers"
|
|
||||||
# return 0
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# if ! $force; then
|
|
||||||
# read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm
|
|
||||||
# case "$confirm" in
|
|
||||||
# [yY][eE][sS]|[yY]) ;;
|
|
||||||
# *) log::info "Aborted"; return 0 ;;
|
|
||||||
# esac
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# local count=0
|
|
||||||
# for peer_name in "${peers_list[@]}"; do
|
|
||||||
# [[ -z "$peer_name" ]] && continue
|
|
||||||
|
|
||||||
# # Skip if peer no longer exists
|
|
||||||
# if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
|
||||||
# log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
|
||||||
# continue
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# cmd::remove::run --name "$peer_name" --force
|
|
||||||
# (( count++ )) || true
|
|
||||||
# done
|
|
||||||
|
|
||||||
# log::wg_success "Removed ${count} peers from WireGuard (group '${name}' definition kept)"
|
|
||||||
# }
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Block / Unblock
|
# Block / Unblock
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,24 @@ Examples:
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
INSPECT_WIDTH=48 # total visible width of section lines
|
||||||
|
INSPECT_LABEL_WIDTH=20
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Private helpers
|
# Private helpers
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
|
||||||
function cmd::inspect::_section() {
|
function cmd::inspect::_section() {
|
||||||
local title="$1"
|
local title="${1:-}" extra="${2:-0}"
|
||||||
printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title"
|
local width=$(( INSPECT_WIDTH + extra ))
|
||||||
|
local title_len=${#title}
|
||||||
|
# Account for "── " (3) + " " (1) before dashes
|
||||||
|
local dash_count=$(( width - title_len - 4 ))
|
||||||
|
[[ $dash_count -lt 2 ]] && dash_count=2
|
||||||
|
local dashes
|
||||||
|
dashes=$(printf '─%.0s' $(seq 1 $dash_count))
|
||||||
|
printf "\n \033[0;37m── %s %s\033[0m\n" "$title" "$dashes"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::inspect::_peer_info() {
|
function cmd::inspect::_peer_info() {
|
||||||
|
|
@ -66,7 +77,7 @@ function cmd::inspect::_peer_info() {
|
||||||
block::has_specific_rules "$name" 2>/dev/null && is_restricted="true"
|
block::has_specific_rules "$name" 2>/dev/null && is_restricted="true"
|
||||||
|
|
||||||
local status last_seen endpoint
|
local status last_seen endpoint
|
||||||
status=$(peers::format_status "$name" "$public_key" \
|
status=$(peers::format_status_verbose "$name" "$public_key" \
|
||||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
||||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
|
|
@ -103,17 +114,18 @@ function cmd::inspect::_peer_info() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd::inspect::_section "Client"
|
cmd::inspect::_section "Client"
|
||||||
ui::row "Name" "$name"
|
printf "\n"
|
||||||
ui::row "IP" "$ip"
|
ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
|
ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Rule" "$rule_display"
|
ui::row "Type" "$(peers::display_type "$type" "$subtype")" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Status" "$(echo -e "$status")"
|
ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Endpoint" "${endpoint:-—}"
|
ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Last seen" "$last_seen"
|
ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "AllowedIPs" "$allowed_ips"
|
ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Public key" "${public_key:-—}"
|
ui::row "AllowedIPs" "$allowed_ips" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Activity (total)" "$activity_total"
|
ui::row "Public key" "${public_key:-—}" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Activity (current)" "$activity_current"
|
ui::row "Activity (total)" "$activity_total" "${INSPECT_LABEL_WIDTH}"
|
||||||
|
ui::row "Activity (current)" "$activity_current" "${INSPECT_LABEL_WIDTH}"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -128,7 +140,8 @@ function cmd::inspect::_rule_info() {
|
||||||
cmd::inspect::_section "Rule: ${rule}"
|
cmd::inspect::_section "Rule: ${rule}"
|
||||||
|
|
||||||
if rule::render_extends_tree "$rule"; then
|
if rule::render_extends_tree "$rule"; then
|
||||||
printf "\n"
|
# printf "\n"
|
||||||
|
: # no-op
|
||||||
else
|
else
|
||||||
# No inheritance — flat view
|
# No inheritance — flat view
|
||||||
rule::render_flat "$rule"
|
rule::render_flat "$rule"
|
||||||
|
|
@ -140,20 +153,44 @@ function cmd::inspect::_blocks_info() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
block::has_file "$name" || return 0
|
block::has_file "$name" || return 0
|
||||||
|
|
||||||
cmd::inspect::_section "Peer Blocks"
|
local blocked_direct
|
||||||
|
|
||||||
local blocked_direct
|
|
||||||
blocked_direct=$(block::is_blocked_direct "$name")
|
blocked_direct=$(block::is_blocked_direct "$name")
|
||||||
|
|
||||||
|
local blocked_groups
|
||||||
|
blocked_groups=$(block::get_groups "$name")
|
||||||
|
|
||||||
|
local rules_output
|
||||||
|
rules_output=$(block::get_rules "$name")
|
||||||
|
|
||||||
|
# Skip if truly empty
|
||||||
|
if [[ "$blocked_direct" != "true" ]] && \
|
||||||
|
ui::empty "$blocked_groups" && \
|
||||||
|
ui::empty "$rules_output"; then
|
||||||
|
block::cleanup "$name" # clean up stale empty file
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Count rules for header
|
||||||
|
local rule_count=0
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -n "$line" ]] && (( rule_count++ )) || true
|
||||||
|
done <<< "$rules_output"
|
||||||
|
|
||||||
|
# Build header like firewall: Blocks (+N)
|
||||||
|
local header_counts=""
|
||||||
|
[[ "$rule_count" -gt 0 ]] && header_counts=" (${rule_count})"
|
||||||
|
[[ "$blocked_direct" == "true" || -n "$blocked_groups" ]] && \
|
||||||
|
header_counts="${header_counts} 🚫"
|
||||||
|
|
||||||
|
cmd::inspect::_section "Blocks${header_counts}"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
[[ "$blocked_direct" == "true" ]] && \
|
[[ "$blocked_direct" == "true" ]] && \
|
||||||
printf " \033[1;31m🚫\033[0m blocked directly\n"
|
printf " \033[1;31m🚫\033[0m blocked directly\n"
|
||||||
|
|
||||||
local blocked_groups
|
|
||||||
blocked_groups=$(block::get_groups "$name")
|
|
||||||
[[ -n "$blocked_groups" ]] && \
|
[[ -n "$blocked_groups" ]] && \
|
||||||
printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups"
|
printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups"
|
||||||
|
|
||||||
block::format_rules "$name"
|
block::format_rules "$name"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,20 +199,22 @@ function cmd::inspect::_group_info() {
|
||||||
|
|
||||||
local groups=()
|
local groups=()
|
||||||
mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name")
|
mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name")
|
||||||
[[ ${#groups[@]} -eq 0 || -z "${groups[0]:-}" ]] && return 0
|
|
||||||
|
|
||||||
ui::section "Groups"
|
ui::empty "${groups[*]}" && return 0
|
||||||
|
|
||||||
if [[ ${#groups[@]} -eq 0 ]] || [[ -z "${groups[0]:-}" ]]; then
|
local count=${#groups[@]}
|
||||||
printf " —\n"
|
cmd::inspect::_section "Groups (${count})"
|
||||||
return 0
|
printf "\n"
|
||||||
fi
|
|
||||||
|
|
||||||
for g in "${groups[@]}"; do
|
for g in "${groups[@]}"; do
|
||||||
[[ -z "$g" ]] && continue
|
[[ -z "$g" ]] && continue
|
||||||
local count
|
local peer_count
|
||||||
count=$(json::count "$(group::path "$g")" "peers")
|
local main_marker=""
|
||||||
printf " %-20s %s peers\n" "$g" "$count"
|
peer_count=$(json::count "$(group::path "$g")" "peers")
|
||||||
|
[[ "$g" == "$(peers::get_main_group "$name")" ]] && \
|
||||||
|
main_marker=" \033[0;33m★\033[0m"
|
||||||
|
printf " \033[0;37m·\033[0m %-20s \033[0;37m%s peers\033[0m%b\n" \
|
||||||
|
"$g" "$peer_count" "$main_marker"
|
||||||
done
|
done
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -196,24 +235,15 @@ function cmd::inspect::_firewall_info() {
|
||||||
rules_output+=("$line")
|
rules_output+=("$line")
|
||||||
done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG)
|
done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG)
|
||||||
|
|
||||||
[[ ${#rules_output[@]} -eq 0 || -z "${rules_output[0]:-}" ]] && return 0
|
ui::empty "${rules_output[*]}" && return 0
|
||||||
|
|
||||||
# printf "\n \033[0;37m── Firewall (\033[0;32m+%d\033[0m \033[0;31m-%d\033[0m) \033[0m%s\n" \
|
|
||||||
# "$accepts" "$drops" "$(printf '─%.0s' {1..28})"
|
|
||||||
|
|
||||||
printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \
|
printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \
|
||||||
"$(color::green "+${accepts}")" \
|
"$(color::green "+${accepts}")" \
|
||||||
"$(color::red "-${drops}")" \
|
"$(color::red "-${drops}")" \
|
||||||
"$(printf '─%.0s' {1..28})"
|
"$(printf '\033[0;37m─%.0s' {1..28})"
|
||||||
|
|
||||||
fw::list_peer_rules "$ip" false
|
fw::list_peer_rules "$ip" false
|
||||||
|
|
||||||
# if [[ ${#rules_output[@]} -gt 0 ]]; then
|
|
||||||
# for line in "${rules_output[@]}"; do
|
|
||||||
# fw::format_rule "$line"
|
|
||||||
# done
|
|
||||||
# fi
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,76 @@ function cmd::list::_iter_confs() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# function cmd::list::_render_row() {
|
||||||
|
# local client_name="$1" ip="$2" type="$3"
|
||||||
|
|
||||||
|
# local pubkey="${p_pubkeys[$client_name]:-}"
|
||||||
|
# local handshake_ts="${wg_handshakes[$pubkey]:-0}"
|
||||||
|
# local is_blocked="${p_blocked[$client_name]:-false}"
|
||||||
|
# local is_restricted="${p_restricted[$client_name]:-false}"
|
||||||
|
# local last_ts="${p_last_ts[$client_name]:-}"
|
||||||
|
|
||||||
|
# # Apply status filters
|
||||||
|
# if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
|
# if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
|
# if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
|
||||||
|
# if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
||||||
|
# if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||||
|
# [[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
||||||
|
|
||||||
|
# if [[ -n "$filter_group" ]]; then
|
||||||
|
# local peer_group="${peer_group_map[$client_name]:-}"
|
||||||
|
# [[ "$peer_group" != "$filter_group" ]] && return 0
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# # Format display values
|
||||||
|
# local status last_seen display_type rule group_display
|
||||||
|
# status=$(peers::format_status "$client_name" "$pubkey" \
|
||||||
|
# "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
|
# last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
||||||
|
# "$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
|
# display_type=$(peers::display_type "$type" "${p_subtypes[$client_name]:-}")
|
||||||
|
# rule="${p_rules[$client_name]:-—}"
|
||||||
|
|
||||||
|
# if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
||||||
|
|
||||||
|
# # Print header on first match
|
||||||
|
# if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
||||||
|
# log::section "WireGuard Clients"
|
||||||
|
# cmd::list::_render_header $has_groups
|
||||||
|
# _list_header_printed=true
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# # Update rule counts for summary (outer scope array)
|
||||||
|
# rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
||||||
|
|
||||||
|
# # Pad status
|
||||||
|
# local padded_status
|
||||||
|
# padded_status=$(ui::pad_status "$status" 25)
|
||||||
|
|
||||||
|
# # Render row
|
||||||
|
# if $has_groups; then
|
||||||
|
# group_display="${peer_group_map[$client_name]:-—}"
|
||||||
|
|
||||||
|
# if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
|
||||||
|
# group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# local rule_col_width=12 group_col_width=12
|
||||||
|
# [[ "$rule" == "—" ]] && rule_col_width=14
|
||||||
|
# [[ "$group_display" == "—" ]] && group_col_width=14
|
||||||
|
# printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
|
||||||
|
# "$client_name" "$ip" "$display_type" "$rule" \
|
||||||
|
# "$group_display" "$padded_status" "$last_seen"
|
||||||
|
# else
|
||||||
|
# local rule_col_width=12
|
||||||
|
# [[ "$rule" == "—" ]] && rule_col_width=14
|
||||||
|
# printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \
|
||||||
|
# "$client_name" "$ip" "$display_type" "$rule" \
|
||||||
|
# "$padded_status" "$last_seen"
|
||||||
|
# fi
|
||||||
|
# }
|
||||||
|
|
||||||
function cmd::list::_render_row() {
|
function cmd::list::_render_row() {
|
||||||
local client_name="$1" ip="$2" type="$3"
|
local client_name="$1" ip="$2" type="$3"
|
||||||
|
|
||||||
|
|
@ -307,8 +377,8 @@ function cmd::list::_render_row() {
|
||||||
[[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
[[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
||||||
|
|
||||||
if [[ -n "$filter_group" ]]; then
|
if [[ -n "$filter_group" ]]; then
|
||||||
local peer_group="${peer_group_map[$client_name]:-}"
|
local all_groups="${peer_group_map[$client_name]:-}"
|
||||||
[[ "$peer_group" != "$filter_group" ]] && return 0
|
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Format display values
|
# Format display values
|
||||||
|
|
@ -329,23 +399,26 @@ function cmd::list::_render_row() {
|
||||||
_list_header_printed=true
|
_list_header_printed=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update rule counts for summary (outer scope array)
|
|
||||||
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
||||||
|
|
||||||
# Pad status
|
|
||||||
local padded_status
|
local padded_status
|
||||||
padded_status=$(ui::pad_status "$status" 25)
|
padded_status=$(ui::pad_status "$status" 25)
|
||||||
|
|
||||||
# Render row
|
|
||||||
if $has_groups; then
|
if $has_groups; then
|
||||||
group_display="${peer_group_map[$client_name]:-—}"
|
# Use main group for display, fall back to first group, then —
|
||||||
|
local main_group="${p_main_groups[$client_name]:-}"
|
||||||
|
if [[ -n "$main_group" ]]; then
|
||||||
|
group_display="$main_group"
|
||||||
|
else
|
||||||
|
group_display="${peer_group_map[$client_name]:-—}"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
|
if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
|
||||||
group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
|
group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local rule_col_width=12 group_col_width=12
|
local rule_col_width=12 group_col_width=12
|
||||||
[[ "$rule" == "—" ]] && rule_col_width=14
|
[[ "$rule" == "—" ]] && rule_col_width=14
|
||||||
[[ "$group_display" == "—" ]] && group_col_width=14
|
[[ "$group_display" == "—" ]] && group_col_width=14
|
||||||
printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
|
printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
|
||||||
"$client_name" "$ip" "$display_type" "$rule" \
|
"$client_name" "$ip" "$display_type" "$rule" \
|
||||||
|
|
@ -365,14 +438,15 @@ function cmd::list::_render_row() {
|
||||||
|
|
||||||
function cmd::list::_precompute_all() {
|
function cmd::list::_precompute_all() {
|
||||||
# Peer data
|
# Peer data
|
||||||
declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=()
|
declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=() p_main_groups=()
|
||||||
while IFS="|" read -r name ip rule subtype last_ts last_evt; do
|
while IFS="|" read -r name ip rule subtype last_ts last_evt main_group; do
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
p_ips["$name"]="$ip"
|
p_ips["$name"]="$ip"
|
||||||
p_rules["$name"]="${rule:-—}"
|
p_rules["$name"]="${rule:-—}"
|
||||||
p_subtypes["$name"]="$subtype"
|
p_subtypes["$name"]="$subtype"
|
||||||
p_last_ts["$name"]="$last_ts"
|
p_last_ts["$name"]="$last_ts"
|
||||||
p_last_evt["$name"]="$last_evt"
|
p_last_evt["$name"]="$last_evt"
|
||||||
|
p_main_groups["$name"]="${main_group:-}"
|
||||||
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
||||||
|
|
||||||
# WireGuard handshakes + endpoints
|
# WireGuard handshakes + endpoints
|
||||||
|
|
@ -399,7 +473,7 @@ function cmd::list::_precompute_all() {
|
||||||
p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
||||||
done
|
done
|
||||||
|
|
||||||
# Groups
|
# Groups + main group
|
||||||
has_groups=false
|
has_groups=false
|
||||||
declare -gA peer_group_map=()
|
declare -gA peer_group_map=()
|
||||||
local groups_dir
|
local groups_dir
|
||||||
|
|
@ -422,6 +496,65 @@ function cmd::list::_precompute_all() {
|
||||||
done < <(json::peer_transfer "$(config::interface)")
|
done < <(json::peer_transfer "$(config::interface)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# function cmd::list::_precompute_all() {
|
||||||
|
# # Peer data
|
||||||
|
# declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=()
|
||||||
|
# while IFS="|" read -r name ip rule subtype last_ts last_evt; do
|
||||||
|
# [[ -z "$name" ]] && continue
|
||||||
|
# p_ips["$name"]="$ip"
|
||||||
|
# p_rules["$name"]="${rule:-—}"
|
||||||
|
# p_subtypes["$name"]="$subtype"
|
||||||
|
# p_last_ts["$name"]="$last_ts"
|
||||||
|
# p_last_evt["$name"]="$last_evt"
|
||||||
|
# done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
||||||
|
|
||||||
|
# # WireGuard handshakes + endpoints
|
||||||
|
# declare -gA wg_handshakes=() wg_endpoints=()
|
||||||
|
# while IFS=$'\t' read -r pubkey ts; do
|
||||||
|
# [[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
||||||
|
# done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
||||||
|
# while IFS=$'\t' read -r pubkey endpoint; do
|
||||||
|
# [[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
||||||
|
# done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
||||||
|
|
||||||
|
# # Block/restricted status
|
||||||
|
# declare -gA p_blocked=() p_restricted=()
|
||||||
|
# cmd::list::_precompute_block_status p_blocked p_restricted
|
||||||
|
|
||||||
|
# # Public keys
|
||||||
|
# declare -gA p_pubkeys=()
|
||||||
|
# local dir
|
||||||
|
# dir="$(ctx::clients)"
|
||||||
|
# for kf in "${dir}"/*_public.key; do
|
||||||
|
# [[ -f "$kf" ]] || continue
|
||||||
|
# local kname
|
||||||
|
# kname=$(basename "$kf" _public.key)
|
||||||
|
# p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
||||||
|
# done
|
||||||
|
|
||||||
|
# # Groups
|
||||||
|
# has_groups=false
|
||||||
|
# declare -gA peer_group_map=()
|
||||||
|
# local groups_dir
|
||||||
|
# groups_dir="$(ctx::groups)"
|
||||||
|
# local group_files=("${groups_dir}"/*.group)
|
||||||
|
# if [[ -f "${group_files[0]}" ]]; then
|
||||||
|
# has_groups=true
|
||||||
|
# while IFS=":" read -r peer_name group_name; do
|
||||||
|
# [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
|
||||||
|
# done < <(json::peer_group_map "$groups_dir")
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# # Transfer/activity data — keyed by pubkey
|
||||||
|
# declare -gA p_rx=() p_tx=() p_activity=()
|
||||||
|
# while IFS="|" read -r pubkey rx tx level; do
|
||||||
|
# [[ -z "$pubkey" ]] && continue
|
||||||
|
# p_rx["$pubkey"]="$rx"
|
||||||
|
# p_tx["$pubkey"]="$tx"
|
||||||
|
# p_activity["$pubkey"]="$level"
|
||||||
|
# done < <(json::peer_transfer "$(config::interface)")
|
||||||
|
# }
|
||||||
|
|
||||||
function cmd::list::_precompute_block_status() {
|
function cmd::list::_precompute_block_status() {
|
||||||
local -n _blocked="$1"
|
local -n _blocked="$1"
|
||||||
local -n _restricted="$2"
|
local -n _restricted="$2"
|
||||||
|
|
|
||||||
|
|
@ -191,11 +191,10 @@ function cmd::unblock::_unblock_all() {
|
||||||
|
|
||||||
# Direct unblock overrides everything — clear all block state
|
# Direct unblock overrides everything — clear all block state
|
||||||
block::set_direct "$name" "$client_ip" "false"
|
block::set_direct "$name" "$client_ip" "false"
|
||||||
|
block::clear_full_block "$name"
|
||||||
|
|
||||||
# Force full unblock regardless of group blocks
|
|
||||||
# (direct unblock = admin override)
|
|
||||||
block::restore_peer "$name" "$client_ip"
|
block::restore_peer "$name" "$client_ip"
|
||||||
block::remove_file "$name"
|
block::cleanup "$name"
|
||||||
|
|
||||||
local rule
|
local rule
|
||||||
rule=$(peers::get_meta "$name" "rule")
|
rule=$(peers::get_meta "$name" "rule")
|
||||||
|
|
|
||||||
105
core/json.sh
105
core/json.sh
|
|
@ -2,63 +2,64 @@
|
||||||
|
|
||||||
JSON_HELPER="${_CTX_ROOT}/core/json_helper.py"
|
JSON_HELPER="${_CTX_ROOT}/core/json_helper.py"
|
||||||
|
|
||||||
function json::get() { python3 "$JSON_HELPER" get "$@" </dev/null; }
|
function json::get() { python3 "$JSON_HELPER" get "$@" </dev/null; }
|
||||||
function json::set() { python3 "$JSON_HELPER" set "$@" </dev/null; }
|
function json::set() { python3 "$JSON_HELPER" set "$@" </dev/null; }
|
||||||
function json::delete() { python3 "$JSON_HELPER" delete "$@" </dev/null; }
|
function json::delete() { python3 "$JSON_HELPER" delete "$@" </dev/null; }
|
||||||
function json::append() { python3 "$JSON_HELPER" append "$@" </dev/null; }
|
function json::append() { python3 "$JSON_HELPER" append "$@" </dev/null; }
|
||||||
function json::remove() { python3 "$JSON_HELPER" remove "$@" </dev/null; }
|
function json::remove() { python3 "$JSON_HELPER" remove "$@" </dev/null; }
|
||||||
function json::cat() { python3 "$JSON_HELPER" cat "$@" </dev/null; }
|
function json::cat() { python3 "$JSON_HELPER" cat "$@" </dev/null; }
|
||||||
function json::has_key() { python3 "$JSON_HELPER" has_key "$@" </dev/null; }
|
function json::has_key() { python3 "$JSON_HELPER" has_key "$@" </dev/null; }
|
||||||
function json::filter_values() { python3 "$JSON_HELPER" filter_values "$@" </dev/null; }
|
function json::filter_values() { python3 "$JSON_HELPER" filter_values "$@" </dev/null; }
|
||||||
function json::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; }
|
function json::last_event() { python3 "$JSON_HELPER" last_event "$@" </dev/null; }
|
||||||
function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </dev/null; }
|
function json::events_for() { python3 "$JSON_HELPER" events_for "$@" </dev/null; }
|
||||||
function json::fw_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" fw_events "$@" </dev/null; }
|
function json::fw_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" fw_events "$@" </dev/null; }
|
||||||
function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" wg_events "$@" </dev/null; }
|
function json::wg_events() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" wg_events "$@" </dev/null; }
|
||||||
function json::format_fw_event() { echo "$1" | python3 "$JSON_HELPER" format_fw_event "$2"; }
|
function json::format_fw_event() { echo "$1" | python3 "$JSON_HELPER" format_fw_event "$2"; }
|
||||||
function json::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
|
function json::format_wg_event() { echo "$1" | python3 "$JSON_HELPER" format_wg_event; }
|
||||||
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
|
function json::remove_events() { python3 "$JSON_HELPER" remove_events "$@" </dev/null; }
|
||||||
function json::follow_logs() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" follow_logs "$@"; }
|
function json::follow_logs() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" follow_logs "$@"; }
|
||||||
function json::count() { python3 "$JSON_HELPER" count "$@" </dev/null; }
|
function json::count() { python3 "$JSON_HELPER" count "$@" </dev/null; }
|
||||||
function json::audit_fw_counts() { python3 "$JSON_HELPER" audit_fw_counts "$@" </dev/null; }
|
function json::audit_fw_counts() { python3 "$JSON_HELPER" audit_fw_counts "$@" </dev/null; }
|
||||||
function json::peer_group_map() { python3 "$JSON_HELPER" peer_group_map "$@" </dev/null; }
|
function json::peer_group_map() { python3 "$JSON_HELPER" peer_group_map "$@" </dev/null; }
|
||||||
function json::peer_groups() { python3 "$JSON_HELPER" peer_groups "$@" </dev/null; }
|
function json::peer_groups() { python3 "$JSON_HELPER" peer_groups "$@" </dev/null; }
|
||||||
function json::peer_data() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" peer_data "$@" </dev/null; }
|
function json::peer_data() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 "$JSON_HELPER" peer_data "$@" </dev/null; }
|
||||||
function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@" </dev/null; }
|
function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@" </dev/null; }
|
||||||
function json::rule_list_data() { python3 "$JSON_HELPER" rule_list_data "$@" </dev/null; }
|
function json::rule_list_data() { python3 "$JSON_HELPER" rule_list_data "$@" </dev/null; }
|
||||||
function json::group_list_data() { python3 "$JSON_HELPER" group_list_data "$@" </dev/null; }
|
function json::group_list_data() { python3 "$JSON_HELPER" group_list_data "$@" </dev/null; }
|
||||||
function json::fmt_datetime() { python3 "$JSON_HELPER" fmt_datetime "$@" </dev/null; }
|
function json::fmt_datetime() { python3 "$JSON_HELPER" fmt_datetime "$@" </dev/null; }
|
||||||
function json::create_rule() { python3 "$JSON_HELPER" create_rule "$@" </dev/null; }
|
function json::create_rule() { python3 "$JSON_HELPER" create_rule "$@" </dev/null; }
|
||||||
function json::cleanup_config() { python3 "$JSON_HELPER" cleanup_config "$@" </dev/null; }
|
function json::cleanup_config() { python3 "$JSON_HELPER" cleanup_config "$@" </dev/null; }
|
||||||
function json::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "$@" </dev/null; }
|
function json::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "$@" </dev/null; }
|
||||||
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; }
|
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; }
|
||||||
function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; }
|
function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; }
|
||||||
function json::parse_fw_event() { python3 "$JSON_HELPER" parse_fw_event "$@" </dev/null; }
|
function json::parse_fw_event() { python3 "$JSON_HELPER" parse_fw_event "$@" </dev/null; }
|
||||||
function json::remove_events_filtered() { python3 "$JSON_HELPER" remove_events_filtered "$@" </dev/null; }
|
function json::remove_events_filtered() { python3 "$JSON_HELPER" remove_events_filtered "$@" </dev/null; }
|
||||||
function json::rule_resolve() { python3 "$JSON_HELPER" rule_resolve "$@" </dev/null; }
|
function json::rule_resolve() { python3 "$JSON_HELPER" rule_resolve "$@" </dev/null; }
|
||||||
function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_field "$@" </dev/null; }
|
function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_field "$@" </dev/null; }
|
||||||
function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; }
|
function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; }
|
||||||
function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; }
|
function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; }
|
||||||
function json::get_raw() { python3 "$JSON_HELPER" get_raw "$@" </dev/null; }
|
function json::get_raw() { python3 "$JSON_HELPER" get_raw "$@" </dev/null; }
|
||||||
function json::count_resolved() { python3 "$JSON_HELPER" count_resolved "$(ctx::rules)" "$@" </dev/null; }
|
function json::count_resolved() { python3 "$JSON_HELPER" count_resolved "$(ctx::rules)" "$@" </dev/null; }
|
||||||
function json::block_get() { python3 "$JSON_HELPER" block_get "$@" </dev/null; }
|
function json::block_get() { python3 "$JSON_HELPER" block_get "$@" </dev/null; }
|
||||||
function json::block_is_blocked() { python3 "$JSON_HELPER" block_is_blocked "$@" </dev/null; }
|
function json::block_is_blocked() { python3 "$JSON_HELPER" block_is_blocked "$@" </dev/null; }
|
||||||
function json::block_set_direct() { python3 "$JSON_HELPER" block_set_direct "$@" </dev/null; }
|
function json::block_set_direct() { python3 "$JSON_HELPER" block_set_direct "$@" </dev/null; }
|
||||||
function json::block_add_group() { python3 "$JSON_HELPER" block_add_group "$@" </dev/null; }
|
function json::block_add_group() { python3 "$JSON_HELPER" block_add_group "$@" </dev/null; }
|
||||||
function json::block_remove_group() { python3 "$JSON_HELPER" block_remove_group "$@" </dev/null; }
|
function json::block_remove_group() { python3 "$JSON_HELPER" block_remove_group "$@" </dev/null; }
|
||||||
function json::block_add_rule() { python3 "$JSON_HELPER" block_add_rule "$@" </dev/null; }
|
function json::block_add_rule() { python3 "$JSON_HELPER" block_add_rule "$@" </dev/null; }
|
||||||
function json::block_remove_rule() { python3 "$JSON_HELPER" block_remove_rule "$@" </dev/null; }
|
function json::block_remove_rule() { python3 "$JSON_HELPER" block_remove_rule "$@" </dev/null; }
|
||||||
function json::block_get_rules() { python3 "$JSON_HELPER" block_get_rules "$@" </dev/null; }
|
function json::block_get_rules() { python3 "$JSON_HELPER" block_get_rules "$@" </dev/null; }
|
||||||
function json::block_get_groups() { python3 "$JSON_HELPER" block_get_groups "$@" </dev/null; }
|
function json::block_get_groups() { python3 "$JSON_HELPER" block_get_groups "$@" </dev/null; }
|
||||||
function json::block_get_direct() { python3 "$JSON_HELPER" block_get_direct "$@" </dev/null; }
|
function json::block_get_direct() { python3 "$JSON_HELPER" block_get_direct "$@" </dev/null; }
|
||||||
function json::net_list() { python3 "$JSON_HELPER" net_list "$@" </dev/null; }
|
function json::net_list() { python3 "$JSON_HELPER" net_list "$@" </dev/null; }
|
||||||
function json::net_show() { python3 "$JSON_HELPER" net_show "$@" </dev/null; }
|
function json::net_show() { python3 "$JSON_HELPER" net_show "$@" </dev/null; }
|
||||||
function json::net_exists() { python3 "$JSON_HELPER" net_exists "$@" </dev/null; }
|
function json::net_exists() { python3 "$JSON_HELPER" net_exists "$@" </dev/null; }
|
||||||
function json::net_add_service() { python3 "$JSON_HELPER" net_add_service "$@" </dev/null; }
|
function json::net_add_service() { python3 "$JSON_HELPER" net_add_service "$@" </dev/null; }
|
||||||
function json::net_add_port() { python3 "$JSON_HELPER" net_add_port "$@" </dev/null; }
|
function json::net_add_port() { python3 "$JSON_HELPER" net_add_port "$@" </dev/null; }
|
||||||
function json::net_remove() { python3 "$JSON_HELPER" net_remove "$@" </dev/null; }
|
function json::net_remove() { python3 "$JSON_HELPER" net_remove "$@" </dev/null; }
|
||||||
function json::net_resolve() { python3 "$JSON_HELPER" net_resolve "$@" </dev/null; }
|
function json::net_resolve() { python3 "$JSON_HELPER" net_resolve "$@" </dev/null; }
|
||||||
function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; }
|
function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; }
|
||||||
function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; }
|
function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; }
|
||||||
|
function json::group_has_peer() { python3 "$JSON_HELPER" group_has_peer "$@" </dev/null; }
|
||||||
|
|
||||||
function json::peer_transfer() {
|
function json::peer_transfer() {
|
||||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||||
|
|
|
||||||
|
|
@ -640,12 +640,13 @@ def peer_data(clients_dir, meta_dir, events_log):
|
||||||
m = meta.get(name, {})
|
m = meta.get(name, {})
|
||||||
rule = m.get('rule', '')
|
rule = m.get('rule', '')
|
||||||
subtype = m.get('subtype', '')
|
subtype = m.get('subtype', '')
|
||||||
|
main_group = m.get('main_group', '')
|
||||||
|
|
||||||
last_event = last_events.get(name, {})
|
last_event = last_events.get(name, {})
|
||||||
last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
|
last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
|
||||||
last_evt = last_event.get('event', '') # fixed: was last_event
|
last_evt = last_event.get('event', '') # fixed: was last_event
|
||||||
|
|
||||||
print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}")
|
print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}|{main_group}")
|
||||||
|
|
||||||
def iso_to_ts(iso_str):
|
def iso_to_ts(iso_str):
|
||||||
"""Convert ISO timestamp to unix timestamp"""
|
"""Convert ISO timestamp to unix timestamp"""
|
||||||
|
|
@ -1270,25 +1271,18 @@ def block_add_rule(file, peer_ip, rule_type, name="", target="",
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def block_remove_rule(file, rule_type, target="", port="", proto=""):
|
def block_remove_rule(file, rule_type, target="", port="", proto=""):
|
||||||
"""Remove matching block rule entry"""
|
data = _block_read(file)
|
||||||
try:
|
if not data:
|
||||||
data = _block_read(file)
|
return
|
||||||
if not data:
|
rules = data.get("rules", [])
|
||||||
return
|
filtered = [r for r in rules if not (
|
||||||
rules = data.get("rules", [])
|
r.get("type") == rule_type and
|
||||||
filtered = []
|
r.get("target", "") == target and
|
||||||
for r in rules:
|
r.get("port", "") == port and
|
||||||
if r.get("type") == rule_type and \
|
r.get("proto", "") == proto
|
||||||
r.get("target", "") == target and \
|
)]
|
||||||
r.get("port", "") == port and \
|
data["rules"] = filtered
|
||||||
r.get("proto", "") == proto:
|
_block_write(file, data)
|
||||||
continue # remove this one
|
|
||||||
filtered.append(r)
|
|
||||||
data["rules"] = filtered
|
|
||||||
_block_write(file, data)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def block_get_rules(file):
|
def block_get_rules(file):
|
||||||
"""Print rules as pipe-separated lines: name|type|target|port|proto"""
|
"""Print rules as pipe-separated lines: name|type|target|port|proto"""
|
||||||
|
|
@ -1483,6 +1477,15 @@ def block_is_empty(file):
|
||||||
)
|
)
|
||||||
print("true" if empty else "false")
|
print("true" if empty else "false")
|
||||||
|
|
||||||
|
def group_has_peer(file, peer_name):
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
peers = data.get('peers', [])
|
||||||
|
print('true' if peer_name in peers else 'false')
|
||||||
|
except Exception:
|
||||||
|
print('false')
|
||||||
|
|
||||||
commands = {
|
commands = {
|
||||||
'get': lambda args: get(args[0], args[1]),
|
'get': lambda args: get(args[0], args[1]),
|
||||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||||
|
|
@ -1573,6 +1576,7 @@ commands = {
|
||||||
args[3] if len(args) > 3 else ''
|
args[3] if len(args) > 3 else ''
|
||||||
),
|
),
|
||||||
'block_is_empty': lambda args: block_is_empty(args[0]),
|
'block_is_empty': lambda args: block_is_empty(args[0]),
|
||||||
|
'group_has_peer': lambda args: group_has_peer(args[0], args[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
48
core/ui.sh
48
core/ui.sh
|
|
@ -41,6 +41,30 @@ function ui::pad() {
|
||||||
printf "%b%${pad}s" "$text" ""
|
printf "%b%${pad}s" "$text" ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ui::pad_mb() {
|
||||||
|
local text="$1" width="${2:-20}"
|
||||||
|
local visible
|
||||||
|
visible=$(printf "%b" "$text" | sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
local vis_len
|
||||||
|
vis_len=$(python3 -c "import sys; print(len(sys.stdin.read().rstrip('\n')))" \
|
||||||
|
<<< "$visible")
|
||||||
|
local pad=$(( width - vis_len ))
|
||||||
|
[[ $pad -lt 0 ]] && pad=0
|
||||||
|
printf "%b%${pad}s" "$text" ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::vis_len_multi() {
|
||||||
|
# Get visible lengths of multiple strings in one Python call
|
||||||
|
# Returns newline-separated integers
|
||||||
|
python3 -c "
|
||||||
|
import sys, re
|
||||||
|
ansi = re.compile(r'\x1b\[[0-9;]*m')
|
||||||
|
for s in sys.argv[1:]:
|
||||||
|
print(len(ansi.sub('', s)))
|
||||||
|
" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function ui::pad_status() {
|
function ui::pad_status() {
|
||||||
ui::pad "${1:-}" "${2:-25}"
|
ui::pad "${1:-}" "${2:-25}"
|
||||||
}
|
}
|
||||||
|
|
@ -64,4 +88,28 @@ function ui::firewall_rule() {
|
||||||
else
|
else
|
||||||
printf "%s\n" "$rule"
|
printf "%s\n" "$rule"
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Content Helpers
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function ui::has_content() {
|
||||||
|
# Returns 0 (true) if content exists, 1 if empty
|
||||||
|
# Works with strings, arrays, or command output
|
||||||
|
local value="${1:-}"
|
||||||
|
[[ -n "$value" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::skip_if_empty() {
|
||||||
|
# Usage: ui::skip_if_empty "$var" || return 0
|
||||||
|
# Or: ui::skip_if_empty "${array[*]}" || return 0
|
||||||
|
local value="${1:-}"
|
||||||
|
[[ -z "${value// }" ]] && return 1 || return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::empty() {
|
||||||
|
# ui::empty "$var" && return 0
|
||||||
|
# ui::empty "${array[*]}" && return 0
|
||||||
|
[[ -z "${1// }" ]]
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"phone-fred": "94.63.0.129",
|
"phone-fred": "94.63.0.129",
|
||||||
"phone-helena": "148.69.46.73",
|
"phone-helena": "148.69.46.73",
|
||||||
"phone-nuno": "148.69.51.201",
|
"phone-nuno": "94.63.0.129",
|
||||||
"tablet-nuno": "148.69.202.5",
|
"tablet-nuno": "148.69.202.5",
|
||||||
"guest-zephyr": "5.13.82.5",
|
"guest-zephyr": "86.120.152.74",
|
||||||
"guest-zephyr-test": "94.63.0.129",
|
"guest-zephyr-test": "94.63.0.129",
|
||||||
"desktop-roboclean": "46.189.215.231",
|
"desktop-roboclean": "46.189.215.231",
|
||||||
"laptop-nuno": "94.63.0.129"
|
"laptop-nuno": "94.63.0.129",
|
||||||
|
"phone-luis": "176.223.61.15"
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +106,14 @@ function block::rename() {
|
||||||
[[ -f "$old_file" ]] && mv "$old_file" "$new_file"
|
[[ -f "$old_file" ]] && mv "$old_file" "$new_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function block::clear_full_block() {
|
||||||
|
local name="${1:?}"
|
||||||
|
local file
|
||||||
|
file=$(block::file "$name")
|
||||||
|
[[ ! -f "$file" ]] && return 0
|
||||||
|
json::block_remove_rule "$file" "full"
|
||||||
|
}
|
||||||
|
|
||||||
# ── High level operations ──────────────────
|
# ── High level operations ──────────────────
|
||||||
|
|
||||||
function block::apply_full() {
|
function block::apply_full() {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,69 @@ function config::_init_defaults() {
|
||||||
_WG_TUNNEL_FULL="0.0.0.0/0, ::/0"
|
_WG_TUNNEL_FULL="0.0.0.0/0, ::/0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Validation
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function config::validate() {
|
||||||
|
local errors=()
|
||||||
|
|
||||||
|
# Required fields
|
||||||
|
local endpoint
|
||||||
|
endpoint=$(config::endpoint)
|
||||||
|
if [[ -z "$endpoint" ]]; then
|
||||||
|
errors+=("WG_ENDPOINT is not set — required for client config generation")
|
||||||
|
elif [[ "$endpoint" != *:* ]]; then
|
||||||
|
errors+=("WG_ENDPOINT must include port (e.g. wg.example.com:51820)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local port
|
||||||
|
port=$(config::port)
|
||||||
|
if [[ -z "$port" ]]; then
|
||||||
|
errors+=("WG_LISTEN_PORT is not set")
|
||||||
|
elif ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
|
||||||
|
errors+=("WG_LISTEN_PORT must be a valid port number (1-65535)")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local dns
|
||||||
|
dns=$(config::dns)
|
||||||
|
if [[ -z "$dns" ]]; then
|
||||||
|
errors+=("WG_DNS is not set — required for client configs")
|
||||||
|
elif ! ip::is_valid "$dns"; then
|
||||||
|
errors+=("WG_DNS must be a valid IP address")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local subnet
|
||||||
|
subnet=$(config::subnet)
|
||||||
|
if [[ -z "$subnet" ]]; then
|
||||||
|
errors+=("WG_SUBNET is not set — required for IP allocation")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local interface
|
||||||
|
interface=$(config::interface)
|
||||||
|
if [[ -z "$interface" ]]; then
|
||||||
|
errors+=("WG_INTERFACE is not set, defaulting to wg0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Warn-only fields
|
||||||
|
local lan
|
||||||
|
lan=$(config::lan)
|
||||||
|
if [[ -z "$lan" ]]; then
|
||||||
|
log::wg_warning "WG_LAN is not set — some rule features may not work correctly"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#errors[@]} -gt 0 ]]; then
|
||||||
|
log::error "wgctl configuration errors:"
|
||||||
|
for err in "${errors[@]}"; do
|
||||||
|
printf " ✗ %s\n" "$err" >&2
|
||||||
|
done
|
||||||
|
printf "\n Edit /etc/wireguard/.wgctl/wgctl.conf to fix these issues.\n\n" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Load overrides from .wgctl/wgctl.conf
|
# Load overrides from .wgctl/wgctl.conf
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -129,10 +192,7 @@ function config::activity_total_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES";
|
||||||
function config::activity_current_low() { echo "$_ACTIVITY_TOTAL_LOW_BYTES"; }
|
function config::activity_current_low() { echo "$_ACTIVITY_TOTAL_LOW_BYTES"; }
|
||||||
function config::activity_current_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; }
|
function config::activity_current_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; }
|
||||||
function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; }
|
function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; }
|
||||||
|
function config::server_public_key() { cat "$_WG_SERVER_PUBLIC_KEY_FILE"; }
|
||||||
function config::server_public_key() {
|
|
||||||
cat "$_WG_SERVER_PUBLIC_KEY_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
function config::device_types() {
|
function config::device_types() {
|
||||||
local types
|
local types
|
||||||
|
|
|
||||||
|
|
@ -83,4 +83,15 @@ function group::each_peer() {
|
||||||
function group::_peer_exists_check() {
|
function group::_peer_exists_check() {
|
||||||
local peer_name="${1:-}"
|
local peer_name="${1:-}"
|
||||||
peers::require_exists "$peer_name" > /dev/null 2>&1
|
peers::require_exists "$peer_name" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function group::has_peer() {
|
||||||
|
local group_name="${1:?}" peer_name="${2:?}"
|
||||||
|
local group_file
|
||||||
|
group_file="$(group::path "$group_name")"
|
||||||
|
[[ ! -f "$group_file" ]] && return 1
|
||||||
|
local result
|
||||||
|
result=$(json::group_has_peer "$group_file" "$peer_name")
|
||||||
|
[[ "$result" == "true" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -286,13 +286,15 @@ function peers::is_offline() {
|
||||||
peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || return 0
|
peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# function peers::is_offline() {
|
function peers::get_main_group() {
|
||||||
# local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}"
|
local name="${1:?}"
|
||||||
# if peers::is_online "$name" "$handshake_ts" "$last_ts"; then
|
peers::get_meta "$name" "main_group"
|
||||||
# return 1
|
}
|
||||||
# fi
|
|
||||||
# return 0
|
function peers::set_main_group() {
|
||||||
# }
|
local name="${1:?}" group="${2:?}"
|
||||||
|
peers::set_meta "$name" "main_group" "$group"
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Name + Type Parsing
|
# Name + Type Parsing
|
||||||
|
|
@ -352,6 +354,22 @@ function peers::format_last_seen() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# function peers::format_status() {
|
||||||
|
# local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
||||||
|
# local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
||||||
|
|
||||||
|
# local state
|
||||||
|
# state=$(peers::connection_state "$is_blocked" "$is_restricted" \
|
||||||
|
# "$handshake_ts" "$last_ts")
|
||||||
|
|
||||||
|
# local conn_str modifier color
|
||||||
|
# IFS="|" read -r conn_str modifier color <<< "$state"
|
||||||
|
|
||||||
|
# local display="$conn_str"
|
||||||
|
# [[ -n "$modifier" ]] && display="${conn_str} (${modifier})"
|
||||||
|
# echo -e "${color}${display}\033[0m"
|
||||||
|
# }
|
||||||
|
|
||||||
function peers::format_status() {
|
function peers::format_status() {
|
||||||
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
||||||
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
||||||
|
|
@ -363,9 +381,47 @@ function peers::format_status() {
|
||||||
local conn_str modifier color
|
local conn_str modifier color
|
||||||
IFS="|" read -r conn_str modifier color <<< "$state"
|
IFS="|" read -r conn_str modifier color <<< "$state"
|
||||||
|
|
||||||
local display="$conn_str"
|
# Color based on state — modifier overrides base connection color
|
||||||
[[ -n "$modifier" ]] && display="${conn_str} (${modifier})"
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
echo -e "${color}${display}\033[0m"
|
color="\033[1;31m" # red — blocked
|
||||||
|
elif [[ "$is_restricted" == "true" ]]; then
|
||||||
|
color="\033[1;33m" # yellow — restricted
|
||||||
|
elif [[ "$conn_str" == "online" ]]; then
|
||||||
|
color="\033[1;32m" # green — online
|
||||||
|
else
|
||||||
|
color="\033[0;37m" # gray — offline
|
||||||
|
fi
|
||||||
|
|
||||||
|
local conn_str_padded
|
||||||
|
conn_str_padded=$(printf "%-7s" "$conn_str")
|
||||||
|
echo -e "${color}${conn_str_padded}\033[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inspect — verbose, color + descriptive text
|
||||||
|
function peers::format_status_verbose() {
|
||||||
|
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
||||||
|
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
||||||
|
|
||||||
|
local conn_str
|
||||||
|
local state
|
||||||
|
state=$(peers::connection_state "$is_blocked" "$is_restricted" \
|
||||||
|
"$handshake_ts" "$last_ts")
|
||||||
|
IFS="|" read -r conn_str _ _ <<< "$state"
|
||||||
|
|
||||||
|
local color suffix=""
|
||||||
|
if [[ "$is_blocked" == "true" ]]; then
|
||||||
|
color="\033[1;31m"
|
||||||
|
suffix=" (blocked)"
|
||||||
|
elif [[ "$is_restricted" == "true" ]]; then
|
||||||
|
color="\033[1;33m"
|
||||||
|
suffix=" (restricted)"
|
||||||
|
elif [[ "$conn_str" == "online" ]]; then
|
||||||
|
color="\033[1;32m"
|
||||||
|
else
|
||||||
|
color="\033[0;37m"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${color}${conn_str}${suffix}\033[0m"
|
||||||
}
|
}
|
||||||
|
|
||||||
function peers::display_type() {
|
function peers::display_type() {
|
||||||
|
|
|
||||||
2
wgctl
2
wgctl
|
|
@ -70,6 +70,8 @@ function wgctl::dispatch() {
|
||||||
|
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
help) wgctl::help; return ;;
|
help) wgctl::help; return ;;
|
||||||
|
shell) : ;;
|
||||||
|
*) config::validate || exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# If alias resolved to service, pass original cmd as subcommand
|
# If alias resolved to service, pass original cmd as subcommand
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue