284 lines
No EOL
8 KiB
Bash
284 lines
No EOL
8 KiB
Bash
#!/usr/bin/env bash
|
|
# identity.command.sh — manage peer identities
|
|
#
|
|
# Subcommands:
|
|
# wgctl identity list
|
|
# wgctl identity show --name <name>
|
|
# wgctl identity add --name <name> --peer <peer>
|
|
# wgctl identity remove --name <name>
|
|
# wgctl identity migrate [--dry-run]
|
|
|
|
# ============================================
|
|
# Lifecycle
|
|
# ============================================
|
|
|
|
function cmd::identity::on_load() {
|
|
flag::register --name
|
|
flag::register --peer
|
|
flag::register --dry-run
|
|
flag::register --force
|
|
}
|
|
|
|
# ============================================
|
|
# Help
|
|
# ============================================
|
|
|
|
function cmd::identity::help() {
|
|
cat <<EOF
|
|
Usage: wgctl identity <subcommand> [options]
|
|
|
|
Manage peer identities.
|
|
|
|
Subcommands:
|
|
list List all identities
|
|
show --name <name> Show identity details and device status
|
|
add --name <name> Manually attach a peer to an identity
|
|
--peer <peer>
|
|
remove --name <name> Remove identity and all associated peers
|
|
migrate [--dry-run] Create identities from existing peer names
|
|
|
|
Examples:
|
|
wgctl identity list
|
|
wgctl identity show --name nuno
|
|
wgctl identity add --name nuno --peer roboclean
|
|
wgctl identity remove --name zephyr
|
|
wgctl identity migrate --dry-run
|
|
wgctl identity migrate
|
|
EOF
|
|
}
|
|
|
|
# ============================================
|
|
# Run
|
|
# ============================================
|
|
|
|
function cmd::identity::run() {
|
|
local subcmd="${1:-list}"
|
|
shift || true
|
|
|
|
case "$subcmd" in
|
|
list) cmd::identity::_list "$@" ;;
|
|
show) cmd::identity::_show "$@" ;;
|
|
add) cmd::identity::_add "$@" ;;
|
|
remove) cmd::identity::_remove "$@" ;;
|
|
migrate) cmd::identity::_migrate "$@" ;;
|
|
--help) cmd::identity::help ;;
|
|
*)
|
|
log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, remove, migrate"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ============================================
|
|
# Subcommands
|
|
# ============================================
|
|
|
|
function cmd::identity::_list() {
|
|
local data
|
|
data=$(identity::list_data)
|
|
|
|
if [[ -z "$data" ]]; then
|
|
log::info "No identities found. Run 'wgctl identity migrate' to create from existing peers."
|
|
return 0
|
|
fi
|
|
|
|
ui::identity::header
|
|
while IFS='|' read -r name peer_count types; do
|
|
ui::identity::row "$name" "$peer_count" "$types"
|
|
done <<< "$data"
|
|
}
|
|
|
|
function cmd::identity::_show() {
|
|
local name=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--name) name="$2"; shift 2 ;;
|
|
--help) cmd::identity::help; return ;;
|
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
|
identity::require_exists "$name" || return 1
|
|
|
|
local data peer_count="0"
|
|
data=$(identity::show_data "$name")
|
|
|
|
while IFS='|' read -r key val type_val index_val; do
|
|
case "$key" in
|
|
name)
|
|
peer_count=$(echo "$data" | grep '^peer_count|' | cut -d'|' -f2)
|
|
ui::identity::detail_name "$val" "$peer_count"
|
|
;;
|
|
peer_count) ;; # consumed above
|
|
device)
|
|
local status=""
|
|
status=$(cmd::identity::_device_status "$val")
|
|
ui::identity::device_row "$val" "$type_val" "$index_val" "$status"
|
|
;;
|
|
esac
|
|
done <<< "$data"
|
|
|
|
echo ""
|
|
}
|
|
|
|
function cmd::identity::_device_status() {
|
|
local peer_name="${1:-}"
|
|
local peer_ip
|
|
peer_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || return 0
|
|
[[ -z "$peer_ip" ]] && return 0
|
|
|
|
local is_blocked is_restricted pubkey handshake_ts
|
|
is_blocked=$(peers::is_blocked "$peer_name")
|
|
is_restricted=$(peers::is_restricted "$peer_name")
|
|
pubkey=$(cat "$(ctx::clients)/${peer_name}.pub" 2>/dev/null) || pubkey=""
|
|
handshake_ts=$(wg show wg0 latest-handshakes 2>/dev/null \
|
|
| awk -v pk="$pubkey" '$1==pk{print $2}') || handshake_ts=0
|
|
|
|
local last_ts last_evt
|
|
last_ts=$(peers::get_meta "$peer_name" "last_ts" 2>/dev/null) || last_ts=""
|
|
last_evt=$(peers::get_meta "$peer_name" "last_evt" 2>/dev/null) || last_evt=""
|
|
|
|
local status
|
|
status=$(peers::format_status \
|
|
"$peer_name" "$pubkey" "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
|
echo " — ${status}"
|
|
}
|
|
|
|
function cmd::identity::_add() {
|
|
local name="" peer=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--name) name="$2"; shift 2 ;;
|
|
--peer) peer="$2"; shift 2 ;;
|
|
--help) cmd::identity::help; return ;;
|
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
|
[[ -z "$peer" ]] && { log::error "Missing required flag: --peer"; return 1; }
|
|
|
|
cmd::identity::_require_peer_exists "$peer" || return 1
|
|
|
|
local peer_type index
|
|
peer_type=$(cmd::identity::_resolve_peer_type "$peer" "$name")
|
|
index=$(identity::next_index "$name" "$peer_type")
|
|
|
|
local id_file
|
|
id_file=$(ctx::identity::path "${name}.identity")
|
|
json::identity_add_peer "$id_file" "$name" "$peer" "$peer_type" "$index" </dev/null
|
|
log::ok "Added '${peer}' to identity '${name}' (${peer_type} #${index})"
|
|
}
|
|
|
|
function cmd::identity::_require_peer_exists() {
|
|
local peer="${1:-}"
|
|
if [[ ! -f "$(ctx::clients)/${peer}.conf" ]]; then
|
|
log::error "Peer '${peer}' not found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
function cmd::identity::_resolve_peer_type() {
|
|
local peer="${1:-}" identity_name="${2:-}"
|
|
local inferred
|
|
inferred=$(identity::infer "$peer")
|
|
if [[ -n "$inferred" ]]; then
|
|
echo "$inferred" | cut -d'|' -f2
|
|
else
|
|
peers::get_meta "$peer" "type" 2>/dev/null || echo "none"
|
|
fi
|
|
}
|
|
|
|
function cmd::identity::_remove() {
|
|
local name="" force=false
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--name) name="$2"; shift 2 ;;
|
|
--force) force=true; shift ;;
|
|
--help) cmd::identity::help; return ;;
|
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
|
|
identity::require_exists "$name" || return 1
|
|
|
|
local peers
|
|
peers=$(identity::peers "$name")
|
|
|
|
if [[ -n "$peers" ]]; then
|
|
local peer_list="${peers//$'\n'/, }"
|
|
log::warn "This will permanently remove identity '${name}' and ALL associated peers:"
|
|
log::warn " ${peer_list}"
|
|
|
|
if ! $force; then
|
|
ui::confirm "Continue?" || { log::info "Aborted"; return 0; }
|
|
fi
|
|
|
|
cmd::identity::_remove_all_peers "$peers"
|
|
peers::reload || return 1
|
|
fi
|
|
|
|
local id_file
|
|
id_file=$(ctx::identity::path "${name}.identity")
|
|
json::identity_remove "$id_file" </dev/null
|
|
log::ok "Identity '${name}' removed"
|
|
}
|
|
|
|
function cmd::identity::_remove_all_peers() {
|
|
local peers="${1:-}"
|
|
while IFS= read -r peer_name; do
|
|
[[ -z "$peer_name" ]] && continue
|
|
cmd::identity::_remove_peer "$peer_name"
|
|
done <<< "$peers"
|
|
}
|
|
|
|
function cmd::identity::_remove_peer() {
|
|
local peer_name="${1:-}"
|
|
local client_ip was_blocked=false
|
|
|
|
client_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || client_ip=""
|
|
peers::is_blocked "$peer_name" && was_blocked=true
|
|
|
|
peers::purge "$peer_name" "$client_ip" "$was_blocked" || return 1
|
|
log::ok "Removed peer '${peer_name}'"
|
|
}
|
|
|
|
function cmd::identity::_migrate() {
|
|
local dry_run="false"
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dry-run) dry_run="true"; shift ;;
|
|
--help) cmd::identity::help; return ;;
|
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ "$dry_run" == "true" ]] && log::info "Dry run — no files will be written"
|
|
echo ""
|
|
|
|
[[ "$dry_run" == "false" ]] && mkdir -p "$(ctx::identities)"
|
|
|
|
local created=0 skipped=0 output
|
|
output=$(json::identity_migrate \
|
|
"$(ctx::identities)" \
|
|
"$(ctx::clients)" \
|
|
"$(ctx::meta)" \
|
|
"$dry_run")
|
|
|
|
while IFS='|' read -r action identity_name peer_name peer_type index; do
|
|
case "$action" in
|
|
create)
|
|
ui::identity::migrate_create "$peer_name" "$identity_name" "$peer_type" "$index"
|
|
(( created++ )) || true
|
|
;;
|
|
skip)
|
|
ui::identity::migrate_skip "$peer_name"
|
|
(( skipped++ )) || true
|
|
;;
|
|
esac
|
|
done <<< "$output"
|
|
|
|
ui::identity::migrate_summary "$created" "$skipped" "$dry_run"
|
|
} |