wgctl/modules/peers.module.sh
2026-05-20 21:49:44 +00:00

560 lines
14 KiB
Bash

#!/usr/bin/env bash
# ============================================
# Client Config
# ============================================
function peers::create_client_config() {
local name="$1"
local type="$2"
local ip="$3"
local allowed_ips="${4:-$(config::allowed_ips_for "split")}"
local conf
conf="$(ctx::clients)/${name}.conf"
if [[ -f "$conf" ]]; then
log::wg_warning "Client config already exists: ${name}"
return 1
fi
local private_key
private_key=$(keys::private "$name")
local server_public_key
server_public_key=$(config::server_public_key)
cat > "$conf" <<EOF
[Interface]
PrivateKey = ${private_key}
Address = ${ip}/32
DNS = $(config::dns)
[Peer]
PublicKey = ${server_public_key}
Endpoint = $(config::endpoint)
AllowedIPs = ${allowed_ips}
PersistentKeepalive = 25
EOF
chmod 600 "$conf"
log::debug "Created client config: ${name} (${ip})"
}
function peers::remove_client_config() {
local name="$1"
local conf
conf="$(ctx::clients)/${name}.conf"
if [[ ! -f "$conf" ]]; then
log::wg_warning "Client config not found: ${name}"
return 1
fi
rm -f "$conf"
log::debug "Removed client config: ${name}"
}
# ============================================
# Server Config (wg0.conf) Peer Management
# ============================================
function peers::cleanup_config() {
json::cleanup_config "$(config::config_file)"
}
function peers::add_to_server() {
local name="${1:?name required}"
local public_key="${2:?public_key required}"
local ip="${3:?ip required}"
local config
config=$(config::config_file)
cat >> "$config" <<EOF
[Peer]
# ${name}
PublicKey = ${public_key}
AllowedIPs = ${ip}/32
EOF
log::debug "Added peer to server config: ${name}"
}
function peers::remove_block() {
local name="${1:?name required}"
json::remove_peer_block "$(config::config_file)" "$name"
}
function peers::remove_from_server() {
local name="${1:?name required}"
peers::remove_block "$name"
peers::cleanup_config
log::debug "Removed peer from server config: ${name}"
}
# ============================================
# Listing
# ============================================
function peers::list() {
local dir
dir="$(ctx::clients)"
if [[ -z "$(ls -A "$dir"/*.conf 2>/dev/null)" ]]; then
log::wg_list "No clients configured"
return 0
fi
for conf in "${dir}"/*.conf; do
local client_name
client_name=$(basename "$conf" .conf)
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local public_key
public_key=$(keys::public "$client_name" 2>/dev/null || echo "unknown")
local type
type=$(peers::get_meta "$client_name" "type" 2>/dev/null)
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
printf " %-30s %-15s %-10s %s\n" \
"$client_name" "$ip" "$type" "$public_key"
done
}
function peers::list_by_type() {
local filter_type="$1"
local dir
dir="$(ctx::clients)"
for conf in "${dir}"/*.conf; do
local client_name
client_name=$(basename "$conf" .conf)
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local type
type=$(peers::get_meta "$client_name" "type" 2>/dev/null)
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
[[ "$type" == "$filter_type" ]] && \
printf " %-30s %-15s\n" "$client_name" "$ip"
done
}
function peers::exists_in_server() {
local name="$1"
grep -q "^# ${name}$" "$(config::config_file)"
}
function peers::is_blocked() {
local name="${1:-}"
block::is_blocked "$name"
}
function peers::is_restricted() {
local name="${1:-}"
block::has_specific_rules "$name" 2>/dev/null
}
# ============================================
# Default Rule
# ============================================
function peers::get_type() {
local name="$1"
local ip
ip=$(peers::get_ip "$name")
[[ -z "$ip" ]] && echo "unknown" && return 0
peers::get_type_from_ip "$ip"
}
function peers::default_rule() {
echo "user"
}
function peers::effective_rule() {
local name="$1"
local rule
rule=$(peers::get_meta "$name" "rule")
echo "${rule:---}"
}
# ============================================
# Query
# ============================================
function peers::all() {
local dir
dir="$(ctx::clients)"
for conf in "${dir}"/*.conf; do
[[ -f "$conf" ]] || continue
basename "$conf" .conf
done
}
function peers::with_rule() {
local rule="$1"
while IFS= read -r name; do
local effective
effective=$(peers::effective_rule "$name")
[[ "$effective" == "$rule" ]] && echo "$name"
done < <(peers::all)
}
function peers::get_ip() {
local name="$1"
grep "^Address" "$(ctx::clients)/${name}.conf" 2>/dev/null \
| awk '{print $3}' | cut -d'/' -f1 || true
}
function peers::find_by_ip() {
local target_ip="$1"
while IFS= read -r name; do
local ip
ip=$(peers::get_ip "$name")
[[ "$ip" == "$target_ip" ]] && echo "$name" && return 0
done < <(peers::all)
}
function peers::is_connected() {
local handshake_ts="${1:-0}"
local now diff threshold
now=$(date +%s)
threshold=$(config::handshake_time_sec)
[[ "$handshake_ts" == "0" || -z "$handshake_ts" ]] && return 1
diff=$(( now - handshake_ts ))
(( diff < threshold ))
}
function peers::is_attempting() {
local last_ts="${1:-}"
[[ -z "$last_ts" ]] && return 1
local now attempt_ts diff threshold
now=$(date +%s)
threshold=$(config::handshake_time_sec)
attempt_ts=$(json::iso_to_ts "$last_ts")
[[ -z "$attempt_ts" || "$attempt_ts" == "0" ]] && return 1
diff=$(( now - attempt_ts ))
(( diff < threshold ))
}
function peers::is_online() {
local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}"
local is_blocked
peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
if [[ "$is_blocked" == "true" ]]; then
peers::is_attempting "$last_ts"
else
peers::is_connected "$handshake_ts"
fi
}
function peers::is_offline() {
local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}"
peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || 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
# ============================================
function peers::resolve_name() {
local name="$1"
local type="${2:-}"
if [[ -n "$type" ]]; then
if ! subnet::exists "$type"; then
log::error "Invalid device type: ${type}"
return 1
fi
echo "${type}-${name}"
else
echo "$name"
fi
}
function peers::require_exists() {
local name="$1"
if [[ ! -f "$(ctx::clients)/${name}.conf" ]]; then
log::error "Client not found: ${name}"
return 1
fi
}
function peers::resolve_and_require() {
local name="$1"
local type="${2:-}"
local resolved
resolved=$(peers::resolve_name "$name" "$type") || return 1
peers::require_exists "$resolved" || return 1
echo "$resolved"
}
function peers::rename_meta() {
local name="${1:-}" new_name="${2:-}"
local old_meta new_meta
old_meta=$(peers::meta_path "$name")
new_meta=$(peers::meta_path "$new_name")
[[ -f "$old_meta" ]] && mv "$old_meta" "$new_meta"
return 0
}
# ============================================
# Cleanup
# ============================================
function peers::purge() {
local name="${1:-}" client_ip="${2:-}" was_blocked="${3:-false}"
[[ -n "$client_ip" ]] && fw::flush_peer "$client_ip"
peers::remove_from_server "$name" || return 1
peers::remove_client_config "$name" || return 1
keys::remove "$name" || return 1
group::remove_peer_from_all "$name" || return 1
if [[ -n "$client_ip" ]] && $was_blocked; then
fw::unblock_all "$client_ip"
fi
block::remove_file "$name" 2>/dev/null || true
peers::remove_meta "$name" 2>/dev/null || true
peers::reload || return 1
}
# ============================================
# Display / Formatting
# ============================================
function peers::format_last_seen() {
local name="${1:-}" pubkey="${2:-}" is_blocked="${3:-false}"
local last_ts="${4:-}" last_evt="${5:-}" handshake_ts="${6:-0}"
local data
data=$(peers::last_seen_data "$is_blocked" "$last_ts" "$handshake_ts")
local ts type
IFS="|" read -r ts type <<< "$data"
case "$type" in
none) echo "—" ;;
dropped) echo "$(fmt::datetime_iso "$ts") (dropped)" ;;
handshake) echo "$(fmt::datetime "$ts") (handshake)" ;;
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"
# 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() {
local type="${1:-}" _subtype="${2:-}"
echo "${type:-unknown}"
}
# ============================================
# Connection data
# ============================================
# Data functions — return raw values
function peers::connection_state() {
# Returns: connected|modifier|color_code
local is_blocked="${1:-false}" is_restricted="${2:-false}"
local handshake_ts="${3:-0}" last_ts="${4:-}"
local threshold
threshold=$(config::handshake_time_sec)
local now
now=$(date +%s)
local connected=false modifier="" color
if [[ "$is_blocked" == "true" ]]; then
local attempt_ts diff
attempt_ts=$(json::iso_to_ts "${last_ts:-0}")
diff=$(( now - attempt_ts ))
(( diff < threshold )) && connected=true
modifier="blocked"
color="\033[1;31m"
elif [[ "$is_restricted" == "true" ]]; then
local diff=$(( now - handshake_ts ))
(( diff < threshold )) && connected=true
modifier="restricted"
color="\033[1;33m"
else
local diff=$(( now - handshake_ts ))
(( diff < threshold )) && connected=true
$connected && color="\033[1;32m" || color="\033[0;37m"
fi
$connected && echo "online|${modifier}|${color}" || echo "offline|${modifier}|${color}"
}
function peers::last_seen_data() {
# Returns: timestamp|type (dropped|handshake|none)
local is_blocked="${1:-false}" last_ts="${2:-}" handshake_ts="${3:-0}"
if [[ "$is_blocked" == "true" ]]; then
if [[ -n "$last_ts" && "$last_ts" != "0" && "$last_ts" != "null" ]]; then
echo "${last_ts}|dropped"
else
echo "|none"
fi
else
if [[ -z "$handshake_ts" || "$handshake_ts" == "0" ]]; then
echo "|none"
else
echo "${handshake_ts}|handshake"
fi
fi
}
function peers::get_type_from_ip() {
local ip="${1:-}"
[[ -z "$ip" ]] && echo "unknown" && return 0
subnet::type_from_ip "$ip"
}
# ============================================
# Activity
# ============================================
# Returns: level|rx|tx
function peers::activity_total() {
local pubkey="${1:-}"
json::peer_transfer "$(config::interface)" | grep "^${pubkey}" | head -1 | cut -d'|' -f2-
}
# Returns: level|rx_rate|tx_rate
function peers::activity_current() {
local pubkey="${1:-}"
json::peer_transfer_delta "$(config::interface)" \
"$(ctx::daemon)/transfer_cache.json" \
| grep "^${pubkey}" | head -1 | cut -d'|' -f2-
}
function peers::format_activity_total() {
local pubkey="${1:-}"
local data
data=$(peers::activity_total "$pubkey")
[[ -z "$data" ]] && echo "—" && return 0
local level rx tx rx_hr tx_hr
IFS="|" read -r rx tx level <<< "$data"
rx_hr=$(numfmt --to=iec "${rx:-0}" 2>/dev/null || echo "0B")
tx_hr=$(numfmt --to=iec "${tx:-0}" 2>/dev/null || echo "0B")
echo "${level:-none} (↓${rx_hr}${tx_hr})"
}
function peers::format_activity_current() {
local pubkey="${1:-}"
local data
data=$(peers::activity_current "$pubkey")
[[ -z "$data" ]] && echo "—" && return 0
local level rx_rate tx_rate rx_hr tx_hr
IFS="|" read -r rx_rate tx_rate level <<< "$data"
[[ "$level" == "unknown" ]] && echo "sampling..." && return 0
local rx_hr tx_hr
rx_hr=$(numfmt --to=iec "${rx_rate:-0}" 2>/dev/null || echo "${rx_rate:-0}")
tx_hr=$(numfmt --to=iec "${tx_rate:-0}" 2>/dev/null || echo "${tx_rate:-0}")
echo "${level} (↓${rx_hr}B/s ↑${tx_hr}B/s)"
}
# ============================================
# Helpers - Meta File
# ============================================
function peers::meta_path() {
local name="$1"
echo "$(ctx::meta)/${name}.meta"
}
function peers::get_meta() {
local name="$1" key="$2"
json::get "$(peers::meta_path "$name")" "$key"
}
function peers::set_meta() {
local name="$1" key="$2" value="$3"
json::set "$(peers::meta_path "$name")" "$key" "$value"
}
function peers::remove_meta() {
local name="$1"
rm -f "$(peers::meta_path "$name")"
}
# ============================================
# Live Reload
# ============================================
function peers::reload() {
wg syncconf "$(config::interface)" <(wg-quick strip "$(config::interface)")
log::debug "WireGuard config reloaded"
}