526 lines
13 KiB
Bash
526 lines
13 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 "$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
|
|
|
|
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"
|
|
}
|