refactor: test suite improvements, peers::get_type, dead code removal, add::run helpers, ui::col_width attempt

This commit is contained in:
Nuno Duque Nunes 2026-05-12 00:49:12 +00:00
parent 312f1f973c
commit 8ca3669c6c
6 changed files with 75 additions and 652 deletions

View file

@ -147,100 +147,71 @@ function cmd::add::run() {
esac
done
# --guest shorthand
if $guest; then
type="guest"
fi
$guest && type="guest"
# Resolve guest subtype
local effective_type="$type"
if [[ "$type" == "guest" && -n "$subtype" ]]; then
# Validate subtype
local valid_subtypes="desktop laptop phone tablet"
if ! echo "$valid_subtypes" | grep -qw "$subtype"; then
log::error "Invalid subtype: ${subtype} (valid: desktop, laptop, phone, tablet)"
return 1
fi
effective_type="guest-${subtype}"
fi
local effective_type
effective_type=$(cmd::add::_resolve_type "$type" "$subtype") || return 1
# Build full client name
local full_name="${type}-${name}"
# Validate
cmd::add::validate "$name" "$type" "$ip" "$tunnel" || return 1
# Resolve tunnel mode — flag > device default
if [[ -z "$tunnel" ]]; then
tunnel=$(config::default_tunnel_for "$type")
fi
[[ -z "$tunnel" ]] && tunnel=$(config::default_tunnel_for "$type")
[[ -z "$rule" ]] && rule=$(cmd::add::_default_rule "$type")
# Determine rule — explicit flag > type default
if [[ -z "$rule" ]]; then
if config::is_guest_type "$type"; then
rule="guest"
else
rule="user"
fi
fi
# Validate rule if specified
if ! rule::exists "$rule"; then
log::error "Rule not found: ${rule}"
return 1
fi
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
local allowed_ips
allowed_ips=$(config::allowed_ips_for "$effective_type" "$tunnel") || return 1
log::section "Adding client: ${full_name}"
# Auto-assign IP if not provided
if [[ -z "$ip" ]]; then
ip=$(ip::next_for_type "$effective_type") || return 1
fi
[[ -z "$ip" ]] && ip=$(ip::next_for_type "$effective_type") || return 1
log::wg_add "Name: ${full_name}"
log::wg_add "Type: ${type}"
log::wg_add "IP: ${ip}"
log::wg_add "Tunnel: ${tunnel} (${allowed_ips})"
log::wg_add "Rule: ${rule:-none}"
# Generate keys
keys::generate_pair "$full_name" || return 1
# Create client config
peers::create_client_config "$full_name" "$effective_type" "$ip" "$allowed_ips" || return 1
[[ -n "$subtype" ]] && peers::set_meta "$full_name" "subtype" "$subtype"
# Store subtype in meta
if [[ -n "$subtype" ]]; then
peers::set_meta "$full_name" "subtype" "$subtype"
fi
# Add peer to server config
local public_key
public_key=$(keys::public "$full_name") || return 1
peers::add_to_server "$full_name" "$public_key" "$ip" || return 1
log::wg_add "Rule: ${rule:-none}"
# Apply presets
for preset in "${presets[@]}"; do
fw::apply_preset "$preset" "${ip}" || return 1
fw::apply_preset "$preset" "$ip" || return 1
done
# Apply rule
if [[ -n "$rule" ]]; then
rule::apply "$rule" "$ip" || return 1
fi
# Reload WireGuard
[[ -n "$rule" ]] && rule::apply "$rule" "$ip" || return 1
peers::reload || return 1
log::wg_success "Client added successfully: ${full_name} (${ip}) [${tunnel} tunnel]"
cmd::add::_show_result "$full_name" "${subtype:-$type}"
}
# Show QR for mobile by default, config for desktop/laptop
local display_type="${subtype:-$type}"
function cmd::add::_resolve_type() {
local type="$1" subtype="$2"
if [[ "$type" == "guest" && -n "$subtype" ]]; then
local valid_subtypes="desktop laptop phone tablet"
if ! echo "$valid_subtypes" | grep -qw "$subtype"; then
log::error "Invalid subtype: ${subtype} (valid: desktop, laptop, phone, tablet)"
return 1
fi
echo "guest-${subtype}"
else
echo "$type"
fi
}
function cmd::add::_default_rule() {
local type="$1"
config::is_guest_type "$type" && echo "guest" || echo "user"
}
function cmd::add::_show_result() {
local full_name="$1" display_type="$2"
if cmd::add::is_mobile "$display_type"; then
keys::qr "$full_name"
else

View file

@ -127,9 +127,13 @@ function cmd::group::list() {
local short_desc="${desc:0:33}"
[[ ${#desc} -gt 33 ]] && short_desc="${short_desc}..."
printf " %-20s %-35s %-8s %b\n" \
local desc_col_width=35
[[ "$desc" == "—" || -z "$desc" ]] && desc_col_width=37
printf " %-20s %-${desc_col_width}s %-8s %b\n" \
"$name" "${short_desc:-}" "$total" \
"${status_color}${status_str}\033[0m"
done < <(json::group_list_data "$groups_dir" "$(ctx::blocks)")
printf "\n"
@ -521,7 +525,11 @@ function cmd::group::block() {
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
if [[ ${#peers_list[@]} -eq 0 ]] || [[ -z "${peers_list[0]:-}" ]]; then
log::wg_warning "Group '${name}' has no peers"
return 0
fi
# log::section "Blocking group: ${name}"

View file

@ -169,7 +169,7 @@ function cmd::list::_format_status() {
echo -e "${color}${conn_str}${modifier}\033[0m"
}
function cmd::list::_get_type() {
function peers::get_type() {
local ip="$1"
local type="unknown"
for t in $(config::device_types); do
@ -277,7 +277,7 @@ function cmd::list::show_client() {
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
local type
type=$(cmd::list::_get_type "$ip")
type=$(peers::get_type "$ip")
local endpoint="—"
if peers::is_blocked "$name"; then
@ -487,7 +487,7 @@ function cmd::list::_iter_confs() {
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
fi
local type
type=$(cmd::list::_get_type "$ip")
type=$(peers::get_type "$ip")
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
"$callback" "$client_name" "$ip" "$type"
done

View file

@ -1,560 +0,0 @@
#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::list::on_load() {
flag::register --type
flag::register --online
flag::register --offline
flag::register --restricted
flag::register --blocked
flag::register --allowed
flag::register --detailed
flag::register --name
}
# ============================================
# Help
# ============================================
function cmd::list::help() {
cat <<EOF
Usage: wgctl list [options]
List all WireGuard clients.
Options:
--type <type> Filter by device type
--online Show only connected clients
--offline Show only disconnected clients
--allowed Show only fully allowed clients
--restricted Show only restricted clients
--blocked Show only blocked clients
--detailed Show full detail cards for all clients
--name <name> Show detail card for a single client
Examples:
wgctl list
wgctl ls --type phone
wgctl list --online
wgctl list --blocked
wgctl list --allowed
wgctl list --restricted
wgctl list --detailed
wgctl list --name phone-nuno
EOF
}
# ============================================
# Status Helpers
# ============================================
function cmd::list::last_handshake_ts() {
local public_key="$1"
wg show "$(config::interface)" latest-handshakes 2>/dev/null \
| grep "^${public_key}" \
| awk '{print $2}'
}
function cmd::list::last_dropped_ts() {
local client_ip="$1"
journalctl -k --grep "wgctl-dropped: " 2>/dev/null \
| grep "SRC=${client_ip}" \
| tail -1 \
| awk '{print $1, $2, $3}'
}
function cmd::list::is_connected() {
local public_key="$1"
local ts
ts=$(cmd::list::last_handshake_ts "$public_key")
[[ -z "$ts" || "$ts" == "0" ]] && return 1
local now diff
now=$(date +%s)
diff=$(( now - ts ))
(( diff < 180 ))
}
function cmd::list::is_attempting() {
local name="$1"
local ts
ts=$(monitor::last_attempt "$name")
[[ -z "$ts" ]] && return 1
local now attempt_ts diff
now=$(date +%s)
attempt_ts=$(python3 -c "
from datetime import datetime, timezone
dt = datetime.fromisoformat('${ts}')
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
print(int(dt.timestamp()))
" 2>/dev/null || echo 0)
diff=$(( now - attempt_ts ))
(( diff < 180 ))
}
function cmd::list::is_blocked() {
local name="$1"
peers::is_blocked "$name"
}
function cmd::list::is_restricted() {
local name="$1"
[[ -f "$(ctx::block::path "${name}.block")" ]]
}
function cmd::list::format_last_seen() {
local name="$1"
local public_key="$2"
local ip="$3"
if cmd::list::is_blocked "$name"; then
local ts
ts=$(monitor::last_attempt "$name")
if [[ -n "$ts" ]]; then
# Format ISO timestamp
local formatted
formatted=$(python3 -c "
from datetime import datetime, timezone
dt = datetime.fromisoformat('${ts}')
print(dt.strftime('%Y-%m-%d %H:%M'))
" 2>/dev/null || echo "$ts")
echo "${formatted} (dropped)"
else
echo "—"
fi
else
local ts
ts=$(cmd::list::last_handshake_ts "$public_key")
if [[ -z "$ts" || "$ts" == "0" ]]; then
echo "—"
else
local formatted
formatted=$(date -d "@${ts}" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$ts")
echo "${formatted} (handshake)"
fi
fi
}
function cmd::list::format_status() {
local name="$1"
local public_key="$2"
local ip="$3" # new
local connected=false
local blocked=false
local restricted=false
cmd::list::is_blocked "$name" && blocked=true
cmd::list::is_restricted "$name" && restricted=true
if $blocked; then
cmd::list::is_attempting "$name" && connected=true
modifier=" (blocked)"
elif $restricted; then
cmd::list::is_connected "$public_key" && connected=true
modifier=" (restricted)"
else
cmd::list::is_connected "$public_key" && connected=true
modifier=""
fi
local conn_str
$connected && conn_str="online" || conn_str="offline"
local status="${conn_str}${modifier}"
local color
if $blocked; then
color="\033[1;31m"
elif $restricted; then
color="\033[1;33m"
elif $connected; then
color="\033[1;32m"
else
color="\033[0;37m"
fi
echo -e "${color}${status}\033[0m"
}
# ============================================
# Display Type
# ============================================
function cmd::list::display_type() {
local name="$1"
local type="$2"
# log::debug "$(config::is_guest_type "$type")"
if config::is_guest_type "$type"; then
local subtype
subtype=$(peers::get_meta "$name" "subtype")
# log::debug "$subtype"
if [[ -n "$subtype" ]]; then
echo "guest/${subtype}"
else
echo "guest"
fi
else
echo "$type"
fi
}
# ============================================
# Detail Card
# ============================================
function cmd::list::show_client() {
local name="$1"
local dir
dir="$(ctx::clients)"
local conf="${dir}/${name}.conf"
if [[ ! -f "$conf" ]]; then
log::error "Client not found: ${name}"
return 1
fi
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local allowed_ips
allowed_ips=$(grep "^AllowedIPs" "$conf" | awk '{print $3}')
local public_key
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
# Get endpoint
local endpoint="—"
if cmd::list::is_blocked "$name"; then
local ep
ep=$(monitor::last_endpoint "$name")
[[ -n "$ep" ]] && endpoint="$ep"
else
local ep
ep=$(monitor::endpoint_for_key "$public_key")
[[ -n "$ep" ]] && endpoint="$ep"
fi
# Determine type
local type="unknown"
for t in $(config::device_types); do
local subnet
subnet=$(config::subnet_for "$t")
if string::starts_with "$ip" "$subnet."; then
type="$t"
break
fi
done
local status
status=$(cmd::list::format_status "$name" "$public_key" "$ip")
local last_seen
last_seen=$(cmd::list::format_last_seen "$name" "$public_key" "$ip")
# Block rules
local block_file
block_file="$(ctx::block::path "${name}.block")"
local blocks=""
if [[ -f "$block_file" ]] && [[ -s "$block_file" ]]; then
while IFS=" " read -r client_ip target port proto; do
if [[ -z "$target" ]]; then
blocks+=" all traffic blocked\n"
else
local rule=" ${target}"
[[ -n "$port" ]] && rule+=":${port}/${proto}"
blocks+="${rule}\n"
fi
done < "$block_file"
fi
local sep
sep="$(printf '─%.0s' {1..50})"
echo ""
echo " ${sep}"
printf " \033[1;34m%-20s\033[0m %s\n" "Client:" "$name"
echo " ${sep}"
printf " %-20s %s\n" "IP:" "$ip"
printf " %-20s %s\n" "Type:" "$type"
printf " %-20s %b\n" "Status:" "$status"
printf " %-20s %s\n" "Endpoint:" "$endpoint"
printf " %-20s %s\n" "Last seen:" "$last_seen"
printf " %-20s %s\n" "Allowed IPs:" "$allowed_ips"
printf " %-20s %s\n" "Public key:" "$public_key"
if [[ -z "$blocks" ]]; then
printf " %-20s %s\n" "Blocks:" "none"
elif [[ "$blocks" == *"all traffic blocked"* ]]; then
printf " %-20s \033[1;31mAll\033[0m\n" "Blocks:"
else
printf " %-20s\n" "Blocks:"
echo -e "$blocks"
fi
echo " ${sep}"
echo ""
}
# ============================================
# Run
# ============================================
function cmd::list::run() {
local filter_type=""
local online_only=false
local offline_only=false
local restricted_only=false
local blocked_only=false
local allowed_only=false
local detailed=false
local single_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--type) filter_type="$2"; shift 2 ;;
--online) online_only=true; shift ;;
--offline) offline_only=true; shift ;;
--restricted) restricted_only=true; shift ;;
--blocked) blocked_only=true; shift ;;
--allowed) allowed_only=true; shift ;;
--detailed) detailed=true; shift ;;
--name) single_name="$2"; shift 2 ;;
--help) cmd::list::help; return ;;
*)
log::error "Unknown flag: $1"
cmd::list::help
return 1
;;
esac
done
# Single client detail card
if [[ -n "$single_name" ]]; then
cmd::list::show_client "$single_name"
return
fi
local dir
dir="$(ctx::clients)"
local confs=("${dir}"/*.conf)
if [[ ! -f "${confs[0]}" ]]; then
log::wg_list "No clients configured"
return 0
fi
# START - GROUP SECTION
# Check if any groups exist
local has_groups=false
local groups_dir
groups_dir="$(ctx::groups)"
local group_files=("${groups_dir}"/*.group)
[[ -f "${group_files[0]}" ]] && has_groups=true
# Precompute peer->group map
declare -A peer_group_map
if $has_groups; then
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
# END - GROUP SECTION
# Detailed mode — cards only, no table
if $detailed; then
log::section "WireGuard Clients"
for conf in "${dir}"/*.conf; do
[[ -f "$conf" ]] || continue
local client_name
client_name=$(basename "$conf" .conf)
# Apply type filter
if [[ -n "$filter_type" ]]; then
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local type="unknown"
for t in $(config::device_types); do
local subnet
subnet=$(config::subnet_for "$t")
if string::starts_with "$ip" "$subnet."; then
type="$t"
break
fi
done
[[ "$type" != "$filter_type" ]] && continue
fi
cmd::list::show_client "$client_name"
done
return
fi
# Normal table view
log::section "WireGuard Clients"
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
for conf in "${dir}"/*.conf; do
[[ -f "$conf" ]] || continue
local client_name
client_name=$(basename "$conf" .conf)
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
# Determine type
local type="unknown"
for t in $(config::device_types); do
local subnet
subnet=$(config::subnet_for "$t")
if string::starts_with "$ip" "$subnet."; then
type="$t"
break
fi
done
# Apply type filter
if [[ -n "$filter_type" && "$type" != "$filter_type" ]]; then
continue
fi
local public_key
public_key=$(keys::public "$client_name" 2>/dev/null || echo "")
# Apply filters
if $online_only && ! cmd::list::is_connected "$public_key"; then
continue
fi
if $offline_only && cmd::list::is_connected "$public_key"; then
continue
fi
if $restricted_only && ! cmd::list::is_restricted "$client_name"; then
continue
fi
if $blocked_only && ! cmd::list::is_blocked "$client_name"; then
continue
fi
if $allowed_only && { cmd::list::is_blocked "$client_name" || cmd::list::is_restricted "$client_name"; }; then
continue
fi
local status
status=$(cmd::list::format_status "$client_name" "$public_key" "$ip")
local last_seen
last_seen=$(cmd::list::format_last_seen "$client_name" "$public_key" "$ip")
local display_type
display_type=$(cmd::list::display_type "$client_name" "$type")
# log::debug "display_type called with name=$client_name type=$type"
local rule
rule=$(peers::effective_rule "$client_name")
rule="${rule:-}"
local padded_status
padded_status=$(cmd::list::pad_status "$status" 25)
local group_display="—"
if $has_groups; then
group_display="${peer_group_map[$client_name]:-}"
fi
local rule_col_width=12
[[ "$rule" == "—" ]] && rule_col_width=14
local group_col_width=12
[[ "$group_display" == "—" ]] && group_col_width=14
if $has_groups; then
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
printf " %-28s %-15s %-13s %-12s %s %s\n" \
"$client_name" "$ip" "$display_type" "$rule" \
"$padded_status" "$last_seen"
fi
done
if $has_groups; then
printf " %s\n" "$(printf '─%.0s' {1..135})"
else
printf " %s\n" "$(printf '─%.0s' {1..107})"
fi
local group_summary=""
if $has_groups; then
declare -A group_counts
for peer in "${!peer_group_map[@]}"; do
local g="${peer_group_map[$peer]}"
group_counts["$g"]=$(( ${group_counts["$g"]:-0} + 1 )) || true
done
for g in "${!group_counts[@]}"; do
group_summary+="${group_counts[$g]} in ${g}, "
done
group_summary="${group_summary%, }"
fi
cmd::list::_render_summary "$group_summary"
printf "\n"
}
function cmd::list::_render_summary() {
local group_summary="${1:-}"
# Summary line
local total online_count
total=$(peers::all | wc -l)
# Count by rule
declare -A rule_summary
while IFS= read -r peer_name; do
local r
r=$(peers::effective_rule "$peer_name")
rule_summary["$r"]=$(( ${rule_summary["$r"]:-0} + 1 ))
done < <(peers::all)
local summary=""
for r in "${!rule_summary[@]}"; do
summary+="${rule_summary[$r]} ${r}, "
done
summary="${summary%, }" # remove trailing comma
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
}
# Strip ANSI codes to measure visible length, then pad manually
function cmd::list::pad_status() {
local status="$1"
local width="${2:-20}"
local visible
visible=$(echo -e "$status" | sed 's/\x1b\[[0-9;]*m//g')
local pad=$(( width - ${#visible} ))
printf "%b%${pad}s" "$status" ""
}

View file

@ -179,39 +179,43 @@ function cmd::test::section_fw() {
function cmd::test::section_destructive() {
test::section "Destructive (modifying state)"
# Cleanup from any previous failed run
/usr/local/bin/wgctl remove --name phone-testunit --force > /dev/null 2>&1 || true
/usr/local/bin/wgctl group remove --name testgroup --force > /dev/null 2>&1 || true
# Add test peer
cmd::test::run_cmd "add phone peer" "added successfully" \
wgctl add --name testunit --type phone --force
add --name testunit --type phone
# Block/unblock
cmd::test::run_cmd "block peer" "blocked" \
wgctl block --name phone-testunit --force
block --name phone-testunit
cmd::test::run_cmd "list shows blocked" "blocked" \
wgctl list --blocked
list --blocked
cmd::test::run_cmd "unblock peer" "unblocked" \
wgctl unblock --name phone-testunit --force
unblock --name phone-testunit
# Rule assign/unassign
cmd::test::run_cmd "rule assign" "Assigned" \
wgctl rule assign --name admin --peer phone-testunit
rule assign --name admin --peer phone-testunit
cmd::test::run_cmd "rule unassign" "Unassigned" \
wgctl rule unassign --peer phone-testunit
rule unassign --peer phone-testunit
# Group operations
cmd::test::run_cmd "group add" "created" \
wgctl group add --name testgroup --desc "Test group"
group add --name testgroup --desc "Test group"
cmd::test::run_cmd "group peer add" "Added" \
wgctl group peer add --name testgroup --peer phone-testunit
cmd::test::run_cmd "group block" "Blocked" \
wgctl group block --name testgroup
cmd::test::run_cmd "group unblock" "Unblocked" \
wgctl group unblock --name testgroup
group peer add --name testgroup --peer phone-testunit
cmd::test::run_cmd "group block" "have been blocked" \
group block --name testgroup
cmd::test::run_cmd "group unblock" "have been unblocked" \
group unblock --name testgroup
cmd::test::run_cmd "group remove" "removed" \
wgctl group remove --name testgroup --force
group remove --name testgroup --force
# Remove test peer
cmd::test::run_cmd "remove phone peer" "removed" \
wgctl remove --name phone-testunit --force
remove --name phone-testunit --force
}
# ============================================

View file

@ -1,9 +1,9 @@
{
"phone-fred": "94.63.0.129",
"phone-helena": "148.69.38.203",
"phone-nuno": "148.69.48.87",
"phone-helena": "148.69.38.9",
"phone-nuno": "94.63.0.129",
"tablet-nuno": "148.69.202.5",
"guest-zephyr": "86.120.152.105",
"guest-zephyr": "5.13.82.5",
"guest-zephyr-test": "94.63.0.129",
"desktop-roboclean": "46.189.215.231"
}