#!/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 "$type" "$(config::default_tunnel_for "$type")")}" 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" <> "$config" </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") # Determine type from IP 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 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 subnet subnet=$(config::subnet_for "$filter_type") if string::starts_with "$ip" "$subnet"; then printf " %-30s %-15s\n" "$client_name" "$ip" fi done } function peers::exists_in_server() { local name="$1" grep -q "^# ${name}$" "$(config::config_file)" } # function peers::is_blocked() { # local name="${1:-}" # peers::exists_in_server "$name" && return 1 || return 0 # } 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 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 echo "$type" } function peers::default_rule() { local name="$1" local type type=$(peers::get_type "$name") config::is_guest_type "$type" && echo "guest" || 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::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 # } # ============================================ # Name + Type Parsing # ============================================ function peers::resolve_name() { local name="$1" local type="${2:-}" if [[ -n "$type" ]]; then if ! config::is_valid_type "$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" } # ============================================ # 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" local display="$conn_str" [[ -n "$modifier" ]] && display="${conn_str} (${modifier})" echo -e "${color}${display}\033[0m" } function peers::display_type() { local type="${1:-}" subtype="${2:-}" if config::is_guest_type "$type" && [[ -n "$subtype" && "$subtype" != "0" ]]; then echo "guest/${subtype}" elif config::is_guest_type "$type"; then echo "guest" else echo "$type" fi } # ============================================ # 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 local type="unknown" for t in $(config::device_types); do local subnet subnet=$(config::subnet_for "$t") string::starts_with "$ip" "${subnet}." && type="$t" && break done echo "$type" } # ============================================ # 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" }