560 lines
No EOL
14 KiB
Bash
560 lines
No EOL
14 KiB
Bash
#!/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" ""
|
|
} |