gistfile1.txt
· 15 KiB · Text
原始檔案
#!/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" <<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")
# 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 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 ! 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"
}
# ============================================
# 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"
# 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:-}"
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"
}
| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | # ============================================ |
| 4 | # Client Config |
| 5 | # ============================================ |
| 6 | |
| 7 | function peers::create_client_config() { |
| 8 | local name="$1" |
| 9 | local type="$2" |
| 10 | local ip="$3" |
| 11 | local allowed_ips="${4:-$(config::allowed_ips_for "$type" "$(config::default_tunnel_for "$type")")}" |
| 12 | |
| 13 | local conf |
| 14 | conf="$(ctx::clients)/${name}.conf" |
| 15 | |
| 16 | if [[ -f "$conf" ]]; then |
| 17 | log::wg_warning "Client config already exists: ${name}" |
| 18 | return 1 |
| 19 | fi |
| 20 | |
| 21 | local private_key |
| 22 | private_key=$(keys::private "$name") |
| 23 | |
| 24 | local server_public_key |
| 25 | server_public_key=$(config::server_public_key) |
| 26 | |
| 27 | cat > "$conf" <<EOF |
| 28 | [Interface] |
| 29 | PrivateKey = ${private_key} |
| 30 | Address = ${ip}/32 |
| 31 | DNS = $(config::dns) |
| 32 | |
| 33 | [Peer] |
| 34 | PublicKey = ${server_public_key} |
| 35 | Endpoint = $(config::endpoint) |
| 36 | AllowedIPs = ${allowed_ips} |
| 37 | PersistentKeepalive = 25 |
| 38 | EOF |
| 39 | |
| 40 | chmod 600 "$conf" |
| 41 | log::debug "Created client config: ${name} (${ip})" |
| 42 | } |
| 43 | |
| 44 | function peers::remove_client_config() { |
| 45 | local name="$1" |
| 46 | local conf |
| 47 | conf="$(ctx::clients)/${name}.conf" |
| 48 | |
| 49 | if [[ ! -f "$conf" ]]; then |
| 50 | log::wg_warning "Client config not found: ${name}" |
| 51 | return 1 |
| 52 | fi |
| 53 | |
| 54 | rm -f "$conf" |
| 55 | log::debug "Removed client config: ${name}" |
| 56 | } |
| 57 | |
| 58 | # ============================================ |
| 59 | # Server Config (wg0.conf) Peer Management |
| 60 | # ============================================ |
| 61 | |
| 62 | function peers::cleanup_config() { |
| 63 | json::cleanup_config "$(config::config_file)" |
| 64 | } |
| 65 | |
| 66 | |
| 67 | function peers::add_to_server() { |
| 68 | local name="${1:?name required}" |
| 69 | local public_key="${2:?public_key required}" |
| 70 | local ip="${3:?ip required}" |
| 71 | |
| 72 | local config |
| 73 | config=$(config::config_file) |
| 74 | |
| 75 | cat >> "$config" <<EOF |
| 76 | |
| 77 | [Peer] |
| 78 | # ${name} |
| 79 | PublicKey = ${public_key} |
| 80 | AllowedIPs = ${ip}/32 |
| 81 | EOF |
| 82 | |
| 83 | log::debug "Added peer to server config: ${name}" |
| 84 | } |
| 85 | |
| 86 | function peers::remove_block() { |
| 87 | local name="${1:?name required}" |
| 88 | json::remove_peer_block "$(config::config_file)" "$name" |
| 89 | } |
| 90 | |
| 91 | function peers::remove_from_server() { |
| 92 | local name="${1:?name required}" |
| 93 | peers::remove_block "$name" |
| 94 | peers::cleanup_config |
| 95 | log::debug "Removed peer from server config: ${name}" |
| 96 | } |
| 97 | |
| 98 | # ============================================ |
| 99 | # Listing |
| 100 | # ============================================ |
| 101 | |
| 102 | function peers::list() { |
| 103 | local dir |
| 104 | dir="$(ctx::clients)" |
| 105 | |
| 106 | if [[ -z "$(ls -A "$dir"/*.conf 2>/dev/null)" ]]; then |
| 107 | log::wg_list "No clients configured" |
| 108 | return 0 |
| 109 | fi |
| 110 | |
| 111 | for conf in "${dir}"/*.conf; do |
| 112 | local client_name |
| 113 | client_name=$(basename "$conf" .conf) |
| 114 | |
| 115 | local ip |
| 116 | ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 117 | |
| 118 | local public_key |
| 119 | public_key=$(keys::public "$client_name" 2>/dev/null || echo "unknown") |
| 120 | |
| 121 | # Determine type from IP |
| 122 | local type="unknown" |
| 123 | for t in $(config::device_types); do |
| 124 | local subnet |
| 125 | subnet=$(config::subnet_for "$t") |
| 126 | if string::starts_with "$ip" "$subnet"; then |
| 127 | type="$t" |
| 128 | break |
| 129 | fi |
| 130 | done |
| 131 | |
| 132 | printf " %-30s %-15s %-10s %s\n" \ |
| 133 | "$client_name" "$ip" "$type" "$public_key" |
| 134 | done |
| 135 | } |
| 136 | |
| 137 | function peers::list_by_type() { |
| 138 | local filter_type="$1" |
| 139 | local dir |
| 140 | dir="$(ctx::clients)" |
| 141 | |
| 142 | for conf in "${dir}"/*.conf; do |
| 143 | local client_name |
| 144 | client_name=$(basename "$conf" .conf) |
| 145 | |
| 146 | local ip |
| 147 | ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 148 | |
| 149 | local subnet |
| 150 | subnet=$(config::subnet_for "$filter_type") |
| 151 | |
| 152 | if string::starts_with "$ip" "$subnet"; then |
| 153 | printf " %-30s %-15s\n" "$client_name" "$ip" |
| 154 | fi |
| 155 | done |
| 156 | } |
| 157 | |
| 158 | function peers::exists_in_server() { |
| 159 | local name="$1" |
| 160 | grep -q "^# ${name}$" "$(config::config_file)" |
| 161 | } |
| 162 | |
| 163 | # function peers::is_blocked() { |
| 164 | # local name="${1:-}" |
| 165 | # peers::exists_in_server "$name" && return 1 || return 0 |
| 166 | # } |
| 167 | |
| 168 | function peers::is_blocked() { |
| 169 | local name="${1:-}" |
| 170 | block::is_blocked "$name" |
| 171 | } |
| 172 | |
| 173 | function peers::is_restricted() { |
| 174 | local name="${1:-}" |
| 175 | block::has_specific_rules "$name" 2>/dev/null |
| 176 | } |
| 177 | |
| 178 | # ============================================ |
| 179 | # Default Rule |
| 180 | # ============================================ |
| 181 | |
| 182 | function peers::get_type() { |
| 183 | local name="$1" |
| 184 | local ip |
| 185 | ip=$(peers::get_ip "$name") |
| 186 | [[ -z "$ip" ]] && echo "unknown" && return 0 |
| 187 | peers::get_type_from_ip "$ip" |
| 188 | } |
| 189 | |
| 190 | function peers::default_rule() { |
| 191 | echo "user" |
| 192 | } |
| 193 | |
| 194 | function peers::effective_rule() { |
| 195 | local name="$1" |
| 196 | local rule |
| 197 | rule=$(peers::get_meta "$name" "rule") |
| 198 | echo "${rule:---}" |
| 199 | } |
| 200 | |
| 201 | # ============================================ |
| 202 | # Query |
| 203 | # ============================================ |
| 204 | |
| 205 | function peers::all() { |
| 206 | local dir |
| 207 | dir="$(ctx::clients)" |
| 208 | for conf in "${dir}"/*.conf; do |
| 209 | [[ -f "$conf" ]] || continue |
| 210 | basename "$conf" .conf |
| 211 | done |
| 212 | } |
| 213 | |
| 214 | function peers::with_rule() { |
| 215 | local rule="$1" |
| 216 | while IFS= read -r name; do |
| 217 | local effective |
| 218 | effective=$(peers::effective_rule "$name") |
| 219 | [[ "$effective" == "$rule" ]] && echo "$name" |
| 220 | done < <(peers::all) |
| 221 | } |
| 222 | |
| 223 | function peers::get_ip() { |
| 224 | local name="$1" |
| 225 | grep "^Address" "$(ctx::clients)/${name}.conf" 2>/dev/null \ |
| 226 | | awk '{print $3}' | cut -d'/' -f1 || true |
| 227 | } |
| 228 | |
| 229 | function peers::find_by_ip() { |
| 230 | local target_ip="$1" |
| 231 | while IFS= read -r name; do |
| 232 | local ip |
| 233 | ip=$(peers::get_ip "$name") |
| 234 | [[ "$ip" == "$target_ip" ]] && echo "$name" && return 0 |
| 235 | done < <(peers::all) |
| 236 | } |
| 237 | |
| 238 | function peers::is_connected() { |
| 239 | local handshake_ts="${1:-0}" |
| 240 | local now diff threshold |
| 241 | now=$(date +%s) |
| 242 | threshold=$(config::handshake_time_sec) |
| 243 | [[ "$handshake_ts" == "0" || -z "$handshake_ts" ]] && return 1 |
| 244 | diff=$(( now - handshake_ts )) |
| 245 | (( diff < threshold )) |
| 246 | } |
| 247 | |
| 248 | function peers::is_attempting() { |
| 249 | local last_ts="${1:-}" |
| 250 | [[ -z "$last_ts" ]] && return 1 |
| 251 | local now attempt_ts diff threshold |
| 252 | now=$(date +%s) |
| 253 | threshold=$(config::handshake_time_sec) |
| 254 | attempt_ts=$(json::iso_to_ts "$last_ts") |
| 255 | [[ -z "$attempt_ts" || "$attempt_ts" == "0" ]] && return 1 |
| 256 | diff=$(( now - attempt_ts )) |
| 257 | (( diff < threshold )) |
| 258 | } |
| 259 | |
| 260 | function peers::is_online() { |
| 261 | local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}" |
| 262 | local is_blocked |
| 263 | peers::is_blocked "$name" && is_blocked="true" || is_blocked="false" |
| 264 | if [[ "$is_blocked" == "true" ]]; then |
| 265 | peers::is_attempting "$last_ts" |
| 266 | else |
| 267 | peers::is_connected "$handshake_ts" |
| 268 | fi |
| 269 | } |
| 270 | |
| 271 | function peers::is_offline() { |
| 272 | local name="${1:-}" handshake_ts="${2:-0}" last_ts="${3:-}" |
| 273 | peers::is_online "$name" "$handshake_ts" "$last_ts" && return 1 || return 0 |
| 274 | } |
| 275 | |
| 276 | function peers::get_main_group() { |
| 277 | local name="${1:?}" |
| 278 | peers::get_meta "$name" "main_group" |
| 279 | } |
| 280 | |
| 281 | function peers::set_main_group() { |
| 282 | local name="${1:?}" group="${2:?}" |
| 283 | peers::set_meta "$name" "main_group" "$group" |
| 284 | } |
| 285 | |
| 286 | # ============================================ |
| 287 | # Name + Type Parsing |
| 288 | # ============================================ |
| 289 | |
| 290 | function peers::resolve_name() { |
| 291 | local name="$1" |
| 292 | local type="${2:-}" |
| 293 | |
| 294 | if [[ -n "$type" ]]; then |
| 295 | if ! config::is_valid_type "$type"; then |
| 296 | log::error "Invalid device type: ${type}" |
| 297 | return 1 |
| 298 | fi |
| 299 | echo "${type}-${name}" |
| 300 | else |
| 301 | echo "$name" |
| 302 | fi |
| 303 | } |
| 304 | |
| 305 | function peers::require_exists() { |
| 306 | local name="$1" |
| 307 | if [[ ! -f "$(ctx::clients)/${name}.conf" ]]; then |
| 308 | log::error "Client not found: ${name}" |
| 309 | return 1 |
| 310 | fi |
| 311 | } |
| 312 | |
| 313 | function peers::resolve_and_require() { |
| 314 | local name="$1" |
| 315 | local type="${2:-}" |
| 316 | |
| 317 | local resolved |
| 318 | resolved=$(peers::resolve_name "$name" "$type") || return 1 |
| 319 | peers::require_exists "$resolved" || return 1 |
| 320 | echo "$resolved" |
| 321 | } |
| 322 | |
| 323 | # ============================================ |
| 324 | # Cleanup |
| 325 | # ============================================ |
| 326 | |
| 327 | function peers::purge() { |
| 328 | local name="${1:-}" client_ip="${2:-}" was_blocked="${3:-false}" |
| 329 | |
| 330 | [[ -n "$client_ip" ]] && fw::flush_peer "$client_ip" |
| 331 | peers::remove_from_server "$name" || return 1 |
| 332 | peers::remove_client_config "$name" || return 1 |
| 333 | keys::remove "$name" || return 1 |
| 334 | group::remove_peer_from_all "$name" || return 1 |
| 335 | |
| 336 | if [[ -n "$client_ip" ]] && $was_blocked; then |
| 337 | fw::unblock_all "$client_ip" |
| 338 | fi |
| 339 | |
| 340 | block::remove_file "$name" 2>/dev/null || true |
| 341 | peers::remove_meta "$name" 2>/dev/null || true |
| 342 | peers::reload || return 1 |
| 343 | } |
| 344 | |
| 345 | |
| 346 | # ============================================ |
| 347 | # Display / Formatting |
| 348 | # ============================================ |
| 349 | |
| 350 | function peers::format_last_seen() { |
| 351 | local name="${1:-}" pubkey="${2:-}" is_blocked="${3:-false}" |
| 352 | local last_ts="${4:-}" last_evt="${5:-}" handshake_ts="${6:-0}" |
| 353 | |
| 354 | local data |
| 355 | data=$(peers::last_seen_data "$is_blocked" "$last_ts" "$handshake_ts") |
| 356 | |
| 357 | local ts type |
| 358 | IFS="|" read -r ts type <<< "$data" |
| 359 | |
| 360 | case "$type" in |
| 361 | none) echo "—" ;; |
| 362 | dropped) echo "$(fmt::datetime_iso "$ts") (dropped)" ;; |
| 363 | handshake) echo "$(fmt::datetime "$ts") (handshake)" ;; |
| 364 | esac |
| 365 | } |
| 366 | |
| 367 | # function peers::format_status() { |
| 368 | # local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}" |
| 369 | # local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}" |
| 370 | |
| 371 | # local state |
| 372 | # state=$(peers::connection_state "$is_blocked" "$is_restricted" \ |
| 373 | # "$handshake_ts" "$last_ts") |
| 374 | |
| 375 | # local conn_str modifier color |
| 376 | # IFS="|" read -r conn_str modifier color <<< "$state" |
| 377 | |
| 378 | # local display="$conn_str" |
| 379 | # [[ -n "$modifier" ]] && display="${conn_str} (${modifier})" |
| 380 | # echo -e "${color}${display}\033[0m" |
| 381 | # } |
| 382 | |
| 383 | function peers::format_status() { |
| 384 | local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}" |
| 385 | local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}" |
| 386 | |
| 387 | local state |
| 388 | state=$(peers::connection_state "$is_blocked" "$is_restricted" \ |
| 389 | "$handshake_ts" "$last_ts") |
| 390 | |
| 391 | local conn_str modifier color |
| 392 | IFS="|" read -r conn_str modifier color <<< "$state" |
| 393 | |
| 394 | # Color based on state — modifier overrides base connection color |
| 395 | if [[ "$is_blocked" == "true" ]]; then |
| 396 | color="\033[1;31m" # red — blocked |
| 397 | elif [[ "$is_restricted" == "true" ]]; then |
| 398 | color="\033[1;33m" # yellow — restricted |
| 399 | elif [[ "$conn_str" == "online" ]]; then |
| 400 | color="\033[1;32m" # green — online |
| 401 | else |
| 402 | color="\033[0;37m" # gray — offline |
| 403 | fi |
| 404 | |
| 405 | local conn_str_padded |
| 406 | conn_str_padded=$(printf "%-7s" "$conn_str") |
| 407 | echo -e "${color}${conn_str_padded}\033[0m" |
| 408 | } |
| 409 | |
| 410 | # Inspect — verbose, color + descriptive text |
| 411 | function peers::format_status_verbose() { |
| 412 | local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}" |
| 413 | local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}" |
| 414 | |
| 415 | local conn_str |
| 416 | local state |
| 417 | state=$(peers::connection_state "$is_blocked" "$is_restricted" \ |
| 418 | "$handshake_ts" "$last_ts") |
| 419 | IFS="|" read -r conn_str _ _ <<< "$state" |
| 420 | |
| 421 | local color suffix="" |
| 422 | if [[ "$is_blocked" == "true" ]]; then |
| 423 | color="\033[1;31m" |
| 424 | suffix=" (blocked)" |
| 425 | elif [[ "$is_restricted" == "true" ]]; then |
| 426 | color="\033[1;33m" |
| 427 | suffix=" (restricted)" |
| 428 | elif [[ "$conn_str" == "online" ]]; then |
| 429 | color="\033[1;32m" |
| 430 | else |
| 431 | color="\033[0;37m" |
| 432 | fi |
| 433 | |
| 434 | echo -e "${color}${conn_str}${suffix}\033[0m" |
| 435 | } |
| 436 | |
| 437 | function peers::display_type() { |
| 438 | local type="${1:-}" _subtype="${2:-}" |
| 439 | echo "${type:-unknown}" |
| 440 | } |
| 441 | |
| 442 | # ============================================ |
| 443 | # Connection data |
| 444 | # ============================================ |
| 445 | |
| 446 | # Data functions — return raw values |
| 447 | function peers::connection_state() { |
| 448 | # Returns: connected|modifier|color_code |
| 449 | local is_blocked="${1:-false}" is_restricted="${2:-false}" |
| 450 | local handshake_ts="${3:-0}" last_ts="${4:-}" |
| 451 | local threshold |
| 452 | threshold=$(config::handshake_time_sec) |
| 453 | local now |
| 454 | now=$(date +%s) |
| 455 | local connected=false modifier="" color |
| 456 | |
| 457 | if [[ "$is_blocked" == "true" ]]; then |
| 458 | local attempt_ts diff |
| 459 | attempt_ts=$(json::iso_to_ts "${last_ts:-0}") |
| 460 | diff=$(( now - attempt_ts )) |
| 461 | (( diff < threshold )) && connected=true |
| 462 | modifier="blocked" |
| 463 | color="\033[1;31m" |
| 464 | elif [[ "$is_restricted" == "true" ]]; then |
| 465 | local diff=$(( now - handshake_ts )) |
| 466 | (( diff < threshold )) && connected=true |
| 467 | modifier="restricted" |
| 468 | color="\033[1;33m" |
| 469 | else |
| 470 | local diff=$(( now - handshake_ts )) |
| 471 | (( diff < threshold )) && connected=true |
| 472 | $connected && color="\033[1;32m" || color="\033[0;37m" |
| 473 | fi |
| 474 | |
| 475 | $connected && echo "online|${modifier}|${color}" || echo "offline|${modifier}|${color}" |
| 476 | } |
| 477 | |
| 478 | function peers::last_seen_data() { |
| 479 | # Returns: timestamp|type (dropped|handshake|none) |
| 480 | local is_blocked="${1:-false}" last_ts="${2:-}" handshake_ts="${3:-0}" |
| 481 | |
| 482 | if [[ "$is_blocked" == "true" ]]; then |
| 483 | if [[ -n "$last_ts" && "$last_ts" != "0" && "$last_ts" != "null" ]]; then |
| 484 | echo "${last_ts}|dropped" |
| 485 | else |
| 486 | echo "|none" |
| 487 | fi |
| 488 | else |
| 489 | if [[ -z "$handshake_ts" || "$handshake_ts" == "0" ]]; then |
| 490 | echo "|none" |
| 491 | else |
| 492 | echo "${handshake_ts}|handshake" |
| 493 | fi |
| 494 | fi |
| 495 | } |
| 496 | |
| 497 | function peers::get_type_from_ip() { |
| 498 | local ip="${1:-}" |
| 499 | [[ -z "$ip" ]] && echo "unknown" && return 0 |
| 500 | subnet::type_from_ip "$ip" |
| 501 | } |
| 502 | |
| 503 | |
| 504 | # ============================================ |
| 505 | # Activity |
| 506 | # ============================================ |
| 507 | |
| 508 | # Returns: level|rx|tx |
| 509 | function peers::activity_total() { |
| 510 | local pubkey="${1:-}" |
| 511 | json::peer_transfer "$(config::interface)" | grep "^${pubkey}" | head -1 | cut -d'|' -f2- |
| 512 | } |
| 513 | |
| 514 | # Returns: level|rx_rate|tx_rate |
| 515 | function peers::activity_current() { |
| 516 | local pubkey="${1:-}" |
| 517 | json::peer_transfer_delta "$(config::interface)" \ |
| 518 | "$(ctx::daemon)/transfer_cache.json" \ |
| 519 | | grep "^${pubkey}" | head -1 | cut -d'|' -f2- |
| 520 | } |
| 521 | |
| 522 | function peers::format_activity_total() { |
| 523 | local pubkey="${1:-}" |
| 524 | local data |
| 525 | data=$(peers::activity_total "$pubkey") |
| 526 | [[ -z "$data" ]] && echo "—" && return 0 |
| 527 | local level rx tx rx_hr tx_hr |
| 528 | IFS="|" read -r rx tx level <<< "$data" |
| 529 | rx_hr=$(numfmt --to=iec "${rx:-0}" 2>/dev/null || echo "0B") |
| 530 | tx_hr=$(numfmt --to=iec "${tx:-0}" 2>/dev/null || echo "0B") |
| 531 | echo "${level:-none} (↓${rx_hr} ↑${tx_hr})" |
| 532 | } |
| 533 | |
| 534 | function peers::format_activity_current() { |
| 535 | local pubkey="${1:-}" |
| 536 | local data |
| 537 | data=$(peers::activity_current "$pubkey") |
| 538 | [[ -z "$data" ]] && echo "—" && return 0 |
| 539 | local level rx_rate tx_rate rx_hr tx_hr |
| 540 | IFS="|" read -r rx_rate tx_rate level <<< "$data" |
| 541 | [[ "$level" == "unknown" ]] && echo "sampling..." && return 0 |
| 542 | local rx_hr tx_hr |
| 543 | rx_hr=$(numfmt --to=iec "${rx_rate:-0}" 2>/dev/null || echo "${rx_rate:-0}") |
| 544 | tx_hr=$(numfmt --to=iec "${tx_rate:-0}" 2>/dev/null || echo "${tx_rate:-0}") |
| 545 | echo "${level} (↓${rx_hr}B/s ↑${tx_hr}B/s)" |
| 546 | } |
| 547 | |
| 548 | # ============================================ |
| 549 | # Helpers - Meta File |
| 550 | # ============================================ |
| 551 | |
| 552 | function peers::meta_path() { |
| 553 | local name="$1" |
| 554 | echo "$(ctx::meta)/${name}.meta" |
| 555 | } |
| 556 | |
| 557 | function peers::get_meta() { |
| 558 | local name="$1" key="$2" |
| 559 | json::get "$(peers::meta_path "$name")" "$key" |
| 560 | } |
| 561 | |
| 562 | function peers::set_meta() { |
| 563 | local name="$1" key="$2" value="$3" |
| 564 | json::set "$(peers::meta_path "$name")" "$key" "$value" |
| 565 | } |
| 566 | |
| 567 | function peers::remove_meta() { |
| 568 | local name="$1" |
| 569 | rm -f "$(peers::meta_path "$name")" |
| 570 | } |
| 571 | |
| 572 | # ============================================ |
| 573 | # Live Reload |
| 574 | # ============================================ |
| 575 | |
| 576 | function peers::reload() { |
| 577 | wg syncconf "$(config::interface)" <(wg-quick strip "$(config::interface)") |
| 578 | log::debug "WireGuard config reloaded" |
| 579 | } |
| 580 |