#!/usr/bin/env bash # ============================================ # Lifecycle # ============================================ function cmd::config::on_load() { flag::register --name flag::register --type flag::register --force flag::register --dry-run } # ============================================ # Help # ============================================ function cmd::config::help() { cat < Show the WireGuard config file for a client. Options: --name Client name (e.g. phone-nuno) Examples: wgctl config --name phone-nuno wgctl config --name laptop-nuno EOF } # ============================================ # Run # ============================================ function cmd::config::run() { local subcmd="${1:-show}" # If first arg is a flag, treat as 'show' subcommand if [[ "$subcmd" == --* ]]; then subcmd="show" else shift || true fi case "$subcmd" in show) cmd::config::_show "$@" ;; migrate) cmd::config::migrate "$@" ;; help) cmd::config::help ;; *) log::error "Unknown subcommand: '${subcmd}'" cmd::config::help return 1 ;; esac } # ============================================ # Show # ============================================ function cmd::config::_show() { local name="" type="" while [[ $# -gt 0 ]]; do case "$1" in --name) name="$2"; shift 2 ;; --type) type="$2"; shift 2 ;; --help) cmd::config::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 name=$(peers::resolve_and_require "$name" "$type") || return 1 local conf conf="$(ctx::clients)/${name}.conf" log::section "Client Config: ${name}" cat "$conf" } # ============================================ # Migrate # ============================================ function cmd::config::migrate() { local force=false dry_run=false while [[ $# -gt 0 ]]; do case "$1" in --force) force=true; shift ;; --dry-run) dry_run=true; shift ;; --help) cmd::config::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done local wgctl_dir wgctl_dir="$(ctx::wgctl)" local config_dir="${wgctl_dir}/config" local data_dir="${wgctl_dir}/data" local legacy_conf="${wgctl_dir}/wgctl.conf" local json_conf="${config_dir}/wgctl.json" # Check if already migrated if [[ -f "$json_conf" && ! -f "$legacy_conf" ]]; then log::wg_warning "Already migrated to new config structure" return 0 fi log::section "wgctl Config Migration" printf "\n" printf " This will:\n" printf " 1. Create %s/config/ and %s/data/\n" "$wgctl_dir" "$wgctl_dir" printf " 2. Convert wgctl.conf → wgctl.json\n" printf " 3. Move data files to data/\n\n" if ! $force && ! $dry_run; then read -r -p " Proceed? [y/N] " confirm case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac fi local do="" $dry_run && do="echo [dry-run]" # 1. Create directories $dry_run || mkdir -p "$config_dir" "$data_dir" $dry_run && printf " Would create: %s/config/\n" "$wgctl_dir" $dry_run && printf " Would create: %s/data/\n" "$wgctl_dir" # 2. Convert wgctl.conf → wgctl.json if [[ -f "$legacy_conf" ]]; then if ! $dry_run; then config::_convert_to_json "$legacy_conf" "$json_conf" fi printf " %s wgctl.conf → config/wgctl.json\n" "$($dry_run && echo '[dry-run]' || echo '✓')" else log::wg_warning "No wgctl.conf found — creating default wgctl.json" $dry_run || config::_write_default_json "$json_conf" fi # 3. Move data files local -a data_files=( "hosts.json" "services.json" "subnets.json" "policies.json" ) local -a data_dirs=( "rules" "identities" "groups" "blocks" "meta" "peer-history" ) for f in "${data_files[@]}"; do if [[ -f "${wgctl_dir}/${f}" ]]; then $dry_run || mv "${wgctl_dir}/${f}" "${data_dir}/${f}" printf " %s %s → data/%s\n" \ "$($dry_run && echo '[dry-run]' || echo '✓')" "$f" "$f" fi done for d in "${data_dirs[@]}"; do if [[ -d "${wgctl_dir}/${d}" ]]; then $dry_run || mv "${wgctl_dir}/${d}" "${data_dir}/${d}" printf " %s %s/ → data/%s/\n" \ "$($dry_run && echo '[dry-run]' || echo '✓')" "$d" "$d" fi done # 4. Remove legacy conf after successful migration if ! $dry_run && [[ -f "$legacy_conf" ]]; then mv "$legacy_conf" "${legacy_conf}.bak" printf " ✓ wgctl.conf → wgctl.conf.bak (backup)\n" fi printf "\n" $dry_run && log::wg_warning "Dry run — no changes made" \ || log::wg_success "Migration complete" } function config::_convert_to_json() { local legacy_file="$1" output_file="$2" # Read legacy conf into variables local wg_interface="wg0" wg_endpoint="" wg_dns="10.0.0.103" local wg_dns_fallback="" wg_port="51820" wg_subnet="10.1.0.0/16" local wg_lan="10.0.0.0/24" wg_hs_check="300" date_format="eu" while IFS='=' read -r key value || [[ -n "$key" ]]; do [[ "$key" =~ ^[[:space:]]*# ]] && continue [[ -z "${key// }" ]] && continue key="${key// /}" value="${value// /}" case "$key" in WG_INTERFACE) wg_interface="$value" ;; WG_ENDPOINT) wg_endpoint="$value" ;; WG_DNS) wg_dns="$value" ;; WG_DNS_FALLBACK) wg_dns_fallback="$value" ;; WG_PORT) wg_port="$value" ;; WG_SUBNET) wg_subnet="$value" ;; WG_LAN) wg_lan="$value" ;; WG_HANDSHAKE_CHECK_TIME_SEC) wg_hs_check="$value" ;; DATE_FORMAT) date_format="$value" ;; esac done < "$legacy_file" # Build fallback DNS array local dns_fallback_json="[]" if [[ -n "$wg_dns_fallback" ]]; then local fallback_array fallback_array=$(echo "$wg_dns_fallback" | tr ',' '\n' | \ while IFS= read -r s; do s="${s// /}" [[ -n "$s" ]] && printf '"%s",' "$s" done | sed 's/,$//') dns_fallback_json="[${fallback_array}]" fi mkdir -p "$(dirname "$output_file")" cat > "$output_file" << JSON { "wireguard": { "interface": "${wg_interface}", "endpoint": "${wg_endpoint}", "port": ${wg_port}, "subnet": "${wg_subnet}", "lan": "${wg_lan}" }, "dns": { "primary": "${wg_dns}", "fallback": ${dns_fallback_json} }, "handshake": { "check_interval_sec": ${wg_hs_check} }, "activity": { "total": {"low": 1000000, "medium": 10000000, "high": 100000000}, "current": {"low": 1000000, "medium": 10000000, "high": 100000000} }, "display": { "date_format": "${date_format}" } } JSON } function config::_write_default_json() { local output_file="$1" mkdir -p "$(dirname "$output_file")" cat > "$output_file" << 'JSON' { "wireguard": { "interface": "wg0", "endpoint": "", "port": 51820, "subnet": "10.1.0.0/16", "lan": "10.0.0.0/24" }, "dns": { "primary": "10.0.0.103", "fallback": [] }, "handshake": { "check_interval_sec": 300 }, "activity": { "total": {"low": 1000000, "medium": 10000000, "high": 100000000}, "current": {"low": 1000000, "medium": 10000000, "high": 100000000} }, "display": { "date_format": "eu" } } JSON }