#!/usr/bin/env bash # commands/export.command.sh function cmd::export::on_load() { flag::register --peer flag::register --identity flag::register --all flag::register --out flag::register --conf-only flag::register --meta-only flag::register --no-config flag::register --no-peers flag::register --force } function cmd::export::help() { cat < Export a single peer (conf, meta, groups, identity, blocks) --identity Export an identity --all Full backup (all peers, rules, identities, groups, etc.) --out Write to file instead of stdout --conf-only Export peer conf only (with --peer) --meta-only Export peer meta only (with --peer) --no-config Skip wgctl.json (with --all) --no-peers Skip peer confs (with --all) --force Overwrite existing output file Examples: wgctl export --peer phone-nuno wgctl export --peer phone-nuno --out phone-nuno.json wgctl export --identity nuno --out nuno.json wgctl export --all --out backup.json wgctl export --all --no-config --out data-only.json EOF } function cmd::export::run() { local peer="" identity="" all=false out="" local conf_only=false meta_only=false local no_config=false no_peers=false force=false while [[ $# -gt 0 ]]; do case "$1" in --peer) peer="$2"; shift 2 ;; --identity) identity="$2"; shift 2 ;; --all) all=true; shift ;; --out) out="$2"; shift 2 ;; --conf-only) conf_only=true; shift ;; --meta-only) meta_only=true; shift ;; --no-config) no_config=true; shift ;; --no-peers) no_peers=true; shift ;; --force) force=true; shift ;; --help) cmd::export::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done # Validate local mode_count=0 [[ -n "$peer" ]] && (( mode_count++ )) || true [[ -n "$identity" ]] && (( mode_count++ )) || true $all && (( mode_count++ )) || true if [[ "$mode_count" -eq 0 ]]; then log::error "Specify --peer, --identity, or --all" cmd::export::help return 1 fi if [[ "$mode_count" -gt 1 ]]; then log::error "Only one of --peer, --identity, --all can be used at a time" return 1 fi # Check output file if [[ -n "$out" && -f "$out" && ! $force ]]; then log::error "Output file already exists: ${out} (use --force to overwrite)" return 1 fi local json="" if [[ -n "$peer" ]]; then json=$(cmd::export::_peer "$peer" "$conf_only" "$meta_only") || return 1 elif [[ -n "$identity" ]]; then json=$(cmd::export::_identity "$identity") || return 1 elif $all; then json=$(cmd::export::_full "$no_config" "$no_peers") || return 1 fi if [[ -n "$out" ]]; then echo "$json" > "$out" log::wg_success "Exported to ${out}" else echo "$json" fi } # ====================================================== # Peer export # ====================================================== function cmd::export::_peer() { local name="${1:-}" conf_only="${2:-false}" meta_only="${3:-false}" peers::require_exists "$name" || return 1 local conf_file conf_file="$(ctx::clients)/${name}.conf" [[ ! -f "$conf_file" ]] && log::error "Client conf not found: ${conf_file}" && return 1 local conf_b64 conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file") if $conf_only; then cmd::export::_envelope "peer_conf" \ "$(printf '{"name":"%s","conf":"%s"}' "$name" "$conf_b64")" return 0 fi # Meta local meta_file meta_json="{}" meta_file="$(ctx::meta)/${name}.meta" [[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file") if $meta_only; then cmd::export::_envelope "peer_meta" \ "$(printf '{"name":"%s","meta":%s}' "$name" "$meta_json")" return 0 fi # Public key local public_key="" local key_file key_file="$(ctx::clients)/${name}_public.key" [[ -f "$key_file" ]] && public_key=$(cat "$key_file") # IP local ip ip=$(peers::get_ip "$name") # Type local peer_type peer_type=$(peers::get_type "$name" 2>/dev/null || echo "") # Direct rule local direct_rule direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "") # Identity local identity identity=$(peers::get_identity "$name" 2>/dev/null || echo "") # Groups local -a group_list=() while IFS= read -r g; do [[ -n "$g" ]] && group_list+=("\"$g\"") done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null) local groups_json="[]" [[ ${#group_list[@]} -gt 0 ]] && \ groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]" # Blocks local block_file is_blocked="false" block_json="null" block_file="$(ctx::blocks)/${name}.block" if [[ -f "$block_file" ]]; then is_blocked="true" block_json=$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file") block_json="\"${block_json}\"" fi local peer_data peer_data=$(printf \ '{"name":"%s","ip":"%s","type":"%s","public_key":"%s","conf":"%s","meta":%s,"identity":"%s","groups":%s,"direct_rule":"%s","blocks":{"is_blocked":%s,"block_file":%s}}' \ "$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \ "$meta_json" "$identity" "$groups_json" "$direct_rule" \ "$is_blocked" "$block_json") cmd::export::_envelope "peer" "$peer_data" } # ====================================================== # Identity export # ====================================================== function cmd::export::_identity() { local name="${1:-}" identity::require_exists "$name" || return 1 local id_file id_file="$(ctx::identities)/${name}.identity" local id_json id_json=$(cat "$id_file") cmd::export::_envelope "identity" \ "$(printf '{"name":"%s","identity":%s}' "$name" "$id_json")" } # ====================================================== # Full backup # ====================================================== function cmd::export::_full() { local no_config="${1:-false}" no_peers="${2:-false}" local version version=$(wgctl::version 2>/dev/null || echo "unknown") python3 "$(ctx::json_helper)" export_full \ "$(ctx::clients)" \ "$(ctx::meta)" \ "$(ctx::rules)" \ "$(ctx::identities)" \ "$(ctx::groups)" \ "$(ctx::blocks)" \ "$(ctx::block_history)" \ "$(ctx::config_file)" \ "$(ctx::policies)" \ "$(ctx::subnets)" \ "$(ctx::net)" \ "$(ctx::hosts)" \ "$no_config" \ "$no_peers" \ "$version" \ 2>/dev/null } # Helper — peer data without envelope (used by full backup) function cmd::export::_peer_data() { local name="${1:-}" local conf_file conf_file="$(ctx::clients)/${name}.conf" [[ ! -f "$conf_file" ]] && return 0 local conf_b64 conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file") local meta_file meta_json="{}" meta_file="$(ctx::meta)/${name}.meta" [[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file") local public_key="" local key_file key_file="$(ctx::clients)/${name}_public.key" [[ -f "$key_file" ]] && public_key=$(cat "$key_file") local ip ip=$(peers::get_ip "$name") local peer_type peer_type=$(peers::get_type "$name" 2>/dev/null || echo "") local direct_rule direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "") local identity identity=$(peers::get_identity "$name" 2>/dev/null || echo "") local -a group_list=() while IFS= read -r g; do [[ -n "$g" ]] && group_list+=("\"$g\"") done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null) local groups_json="[]" [[ ${#group_list[@]} -gt 0 ]] && \ groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]" local block_file is_blocked="false" block_json="null" block_file="$(ctx::blocks)/${name}.block" if [[ -f "$block_file" ]]; then is_blocked="true" block_json="\"$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file")\"" fi printf \ '{"name":"%s","ip":"%s","type":"%s","public_key":"%s","conf":"%s","meta":%s,"identity":"%s","groups":%s,"direct_rule":"%s","blocks":{"is_blocked":%s,"block_file":%s}}' \ "$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \ "$meta_json" "$identity" "$groups_json" "$direct_rule" \ "$is_blocked" "$block_json" } # ====================================================== # Envelope helper # ====================================================== function cmd::export::_envelope() { local export_type="${1:-}" data="${2:-}" local version ts version=$(wgctl::version 2>/dev/null || echo "unknown") ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ") printf '{"wgctl_version":"%s","export_type":"%s","exported_at":"%s","data":%s}\n' \ "$version" "$export_type" "$ts" "$data" } function cmd::export::_compact_json() { local file="$1" python3 -c " import json, sys try: print(json.dumps(json.load(open('${file}')))) except Exception as e: print('{}', file=sys.stderr) " 2>/dev/null }