diff --git a/commands/block.command.sh b/commands/block.command.sh index 4798a5e..1d737eb 100644 --- a/commands/block.command.sh +++ b/commands/block.command.sh @@ -94,7 +94,7 @@ function cmd::block::run() { # Full block if no specific targets if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \ ${#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}" return 0 fi diff --git a/commands/group.command.sh b/commands/group.command.sh index 35cc8b4..d71f756 100644 --- a/commands/group.command.sh +++ b/commands/group.command.sh @@ -11,6 +11,7 @@ function cmd::group::on_load() { flag::register --type flag::register --rule flag::register --new-name + flag::register --main flag::register --force } @@ -83,6 +84,7 @@ function cmd::group::run() { rename) cmd::group::rename "$@" ;; peer) cmd::group::peer "$@" ;; rm-peers) cmd::group::rm_peers "$@" ;; + set-main) cmd::group::set_main "$@" ;; block) cmd::group::block "$@" ;; unblock) cmd::group::unblock "$@" ;; rule) cmd::group::rule "$@" ;; @@ -343,13 +345,14 @@ function cmd::group::peer() { } function cmd::group::peer_add() { - local name="" peer="" type="" + local name="" peer="" type="" set_main=false while [[ $# -gt 0 ]]; do case "$1" in --name) util::require_flag "--name" "${2:-}" || return 1; name="$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 ;; + --main) util::require_flag "--main" "${2:-}" || return 1; set_main=true; shift ;; --help) cmd::group::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac @@ -369,6 +372,11 @@ function cmd::group::peer_add() { group::add_peer "$name" "$peer" 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() { @@ -393,6 +401,34 @@ function cmd::group::peer_remove() { 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 # ============================================ @@ -444,56 +480,6 @@ function cmd::group::_rm_peer_cb() { 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 # ============================================ diff --git a/commands/inspect.command.sh b/commands/inspect.command.sh index 60bf17e..dd0bea6 100644 --- a/commands/inspect.command.sh +++ b/commands/inspect.command.sh @@ -36,13 +36,24 @@ Examples: EOF } +INSPECT_WIDTH=48 # total visible width of section lines +INSPECT_LABEL_WIDTH=20 + # ============================================ # Private helpers # ============================================ + function cmd::inspect::_section() { - local title="$1" - printf "\n \033[0;37m── %s ──────────────────────────────────\033[0m\n" "$title" + local title="${1:-}" extra="${2:-0}" + 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() { @@ -66,7 +77,7 @@ function cmd::inspect::_peer_info() { block::has_specific_rules "$name" 2>/dev/null && is_restricted="true" 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") last_seen=$(peers::format_last_seen "$name" "$public_key" \ "$is_blocked" "$last_ts" "" "$handshake_ts") @@ -103,17 +114,18 @@ function cmd::inspect::_peer_info() { fi cmd::inspect::_section "Client" - ui::row "Name" "$name" - ui::row "IP" "$ip" - ui::row "Type" "$(peers::display_type "$type" "$subtype")" - ui::row "Rule" "$rule_display" - ui::row "Status" "$(echo -e "$status")" - ui::row "Endpoint" "${endpoint:-—}" - ui::row "Last seen" "$last_seen" - ui::row "AllowedIPs" "$allowed_ips" - ui::row "Public key" "${public_key:-—}" - ui::row "Activity (total)" "$activity_total" - ui::row "Activity (current)" "$activity_current" + printf "\n" + ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}" + ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}" + ui::row "Type" "$(peers::display_type "$type" "$subtype")" "${INSPECT_LABEL_WIDTH}" + ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}" + ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}" + ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}" + ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}" + ui::row "AllowedIPs" "$allowed_ips" "${INSPECT_LABEL_WIDTH}" + ui::row "Public key" "${public_key:-—}" "${INSPECT_LABEL_WIDTH}" + ui::row "Activity (total)" "$activity_total" "${INSPECT_LABEL_WIDTH}" + ui::row "Activity (current)" "$activity_current" "${INSPECT_LABEL_WIDTH}" return 0 } @@ -128,7 +140,8 @@ function cmd::inspect::_rule_info() { cmd::inspect::_section "Rule: ${rule}" if rule::render_extends_tree "$rule"; then - printf "\n" + # printf "\n" + : # no-op else # No inheritance — flat view rule::render_flat "$rule" @@ -140,20 +153,44 @@ function cmd::inspect::_blocks_info() { local name="${1:-}" block::has_file "$name" || return 0 - cmd::inspect::_section "Peer Blocks" - - local blocked_direct + local blocked_direct 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" ]] && \ printf " \033[1;31m🚫\033[0m blocked directly\n" - - local blocked_groups - blocked_groups=$(block::get_groups "$name") [[ -n "$blocked_groups" ]] && \ printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups" block::format_rules "$name" - return 0 } @@ -162,20 +199,22 @@ function cmd::inspect::_group_info() { local groups=() 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 - printf " —\n" - return 0 - fi + local count=${#groups[@]} + cmd::inspect::_section "Groups (${count})" + printf "\n" for g in "${groups[@]}"; do [[ -z "$g" ]] && continue - local count - count=$(json::count "$(group::path "$g")" "peers") - printf " %-20s %s peers\n" "$g" "$count" + local peer_count + local main_marker="" + 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 return 0 @@ -196,24 +235,15 @@ function cmd::inspect::_firewall_info() { rules_output+=("$line") done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG) - [[ ${#rules_output[@]} -eq 0 || -z "${rules_output[0]:-}" ]] && 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})" + ui::empty "${rules_output[*]}" && return 0 printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \ "$(color::green "+${accepts}")" \ "$(color::red "-${drops}")" \ - "$(printf '─%.0s' {1..28})" + "$(printf '\033[0;37m─%.0s' {1..28})" 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 } diff --git a/commands/list.command.sh b/commands/list.command.sh index f9010e5..69cbd46 100644 --- a/commands/list.command.sh +++ b/commands/list.command.sh @@ -289,6 +289,76 @@ function cmd::list::_iter_confs() { 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() { local client_name="$1" ip="$2" type="$3" @@ -307,8 +377,8 @@ function cmd::list::_render_row() { [[ "$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 + local all_groups="${peer_group_map[$client_name]:-}" + [[ "$all_groups" != *"$filter_group"* ]] && return 0 fi # Format display values @@ -329,23 +399,26 @@ function cmd::list::_render_row() { _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]:-—}" + # 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 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 + [[ "$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" \ @@ -365,14 +438,15 @@ function cmd::list::_render_row() { 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 + 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 main_group; 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" + p_main_groups["$name"]="${main_group:-}" done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)") # WireGuard handshakes + endpoints @@ -399,7 +473,7 @@ function cmd::list::_precompute_all() { p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "") done - # Groups + # Groups + main group has_groups=false declare -gA peer_group_map=() local groups_dir @@ -422,6 +496,65 @@ function cmd::list::_precompute_all() { 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() { local -n _blocked="$1" local -n _restricted="$2" diff --git a/commands/unblock.command.sh b/commands/unblock.command.sh index cf1e5e2..d5984b2 100644 --- a/commands/unblock.command.sh +++ b/commands/unblock.command.sh @@ -191,11 +191,10 @@ function cmd::unblock::_unblock_all() { # Direct unblock overrides everything — clear all block state 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::remove_file "$name" + block::cleanup "$name" local rule rule=$(peers::get_meta "$name" "rule") diff --git a/core/json.sh b/core/json.sh index 80837cc..9f0dc78 100644 --- a/core/json.sh +++ b/core/json.sh @@ -2,63 +2,64 @@ JSON_HELPER="${_CTX_ROOT}/core/json_helper.py" -function json::get() { python3 "$JSON_HELPER" get "$@" 3 else '' ), '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__': diff --git a/core/ui.sh b/core/ui.sh index c8d87e6..c1be533 100644 --- a/core/ui.sh +++ b/core/ui.sh @@ -41,6 +41,30 @@ function ui::pad() { 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() { ui::pad "${1:-}" "${2:-25}" } @@ -64,4 +88,28 @@ function ui::firewall_rule() { else printf "%s\n" "$rule" 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// }" ]] } \ No newline at end of file diff --git a/daemon/endpoint_cache.json b/daemon/endpoint_cache.json index 154ffbd..947699e 100644 --- a/daemon/endpoint_cache.json +++ b/daemon/endpoint_cache.json @@ -1,10 +1,11 @@ { "phone-fred": "94.63.0.129", "phone-helena": "148.69.46.73", - "phone-nuno": "148.69.51.201", + "phone-nuno": "94.63.0.129", "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", "desktop-roboclean": "46.189.215.231", - "laptop-nuno": "94.63.0.129" + "laptop-nuno": "94.63.0.129", + "phone-luis": "176.223.61.15" } \ No newline at end of file diff --git a/modules/block.module.sh b/modules/block.module.sh index 5e842a9..58d5295 100644 --- a/modules/block.module.sh +++ b/modules/block.module.sh @@ -106,6 +106,14 @@ function block::rename() { [[ -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 ────────────────── function block::apply_full() { diff --git a/modules/config.module.sh b/modules/config.module.sh index 3f9fafe..287c058 100644 --- a/modules/config.module.sh +++ b/modules/config.module.sh @@ -41,6 +41,69 @@ function config::_init_defaults() { _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 # ============================================ @@ -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_med() { echo "$_ACTIVITY_TOTAL_MED_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() { local types diff --git a/modules/group.module.sh b/modules/group.module.sh index 5290aee..83829ad 100644 --- a/modules/group.module.sh +++ b/modules/group.module.sh @@ -83,4 +83,15 @@ function group::each_peer() { function group::_peer_exists_check() { local peer_name="${1:-}" peers::require_exists "$peer_name" > /dev/null 2>&1 -} \ No newline at end of file +} + +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" ]] +} + diff --git a/modules/peers.module.sh b/modules/peers.module.sh index d32dc12..44216bf 100644 --- a/modules/peers.module.sh +++ b/modules/peers.module.sh @@ -286,13 +286,15 @@ function peers::is_offline() { peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || return 0 } -# function peers::is_offline() { -# local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}" -# if peers::is_online "$name" "$handshake_ts" "$last_ts"; then -# return 1 -# fi -# return 0 -# } +function peers::get_main_group() { + local name="${1:?}" + peers::get_meta "$name" "main_group" +} + +function peers::set_main_group() { + local name="${1:?}" group="${2:?}" + peers::set_meta "$name" "main_group" "$group" +} # ============================================ # Name + Type Parsing @@ -352,6 +354,22 @@ function peers::format_last_seen() { 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() { local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}" 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 IFS="|" read -r conn_str modifier color <<< "$state" - local display="$conn_str" - [[ -n "$modifier" ]] && display="${conn_str} (${modifier})" - echo -e "${color}${display}\033[0m" + # Color based on state — modifier overrides base connection color + if [[ "$is_blocked" == "true" ]]; then + 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() { diff --git a/wgctl b/wgctl index 3cbd939..2d19499 100755 --- a/wgctl +++ b/wgctl @@ -70,6 +70,8 @@ function wgctl::dispatch() { case "$cmd" in help) wgctl::help; return ;; + shell) : ;; + *) config::validate || exit 1 ;; esac # If alias resolved to service, pass original cmd as subcommand