#!/usr/bin/env bash # ============================================ # Lifecycle # ============================================ function config::on_load() { config::_init_defaults config::load config::validate fmt::set_date_format "${_FMT_DATE_FORMAT:-eu}" } # ============================================ # Defaults # ============================================ declare -g _ACTIVITY_TOTAL_LOW_BYTES="${ACTIVITY_TOTAL_LOW_BYTES:-1000000}" declare -g _ACTIVITY_TOTAL_MED_BYTES="${ACTIVITY_TOTAL_MED_BYTES:-10000000}" declare -g _ACTIVITY_TOTAL_HIGH_BYTES="${ACTIVITY_TOTAL_HIGH_BYTES:-100000000}" declare -g _ACTIVITY_CURRENT_LOW_BYTES="${ACTIVITY_CURRENT_LOW_BYTES:-1000000}" declare -g _ACTIVITY_CURRENT_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}" declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}" function config::_init_defaults() { _WG_INTERFACE="wg0" _WG_DNS="10.0.0.103" _WG_DNS_FALLBACK="" _WG_LAN="10.0.0.0/24" _WG_SUBNET="10.1.0.0/16" _WG_PORT="51820" _WG_ENDPOINT="" _WG_HANDSHAKE_CHECK_TIME_SEC="300" _FMT_DATE_FORMAT="eu" # Derived _WG_CONFIG="$(ctx::wg)/${_WG_INTERFACE}.conf" _WG_SERVER_PUBLIC_KEY_FILE="$(ctx::wg)/server_public.key" _WG_SERVER_PRIVATE_KEY_FILE="$(ctx::wg)/server_private.key" _WG_TUNNEL_SPLIT="${_WG_SUBNET}, ${_WG_LAN}" _WG_TUNNEL_FULL="0.0.0.0/0, ::/0" } # ============================================ # Load from wgctl.json # Falls back to wgctl.conf for migration period # ============================================ function config::load() { local json_conf json_conf="$(ctx::config_file)" if [[ -f "$json_conf" ]]; then config::_load_json "$json_conf" else # Fallback: legacy wgctl.conf local legacy_conf legacy_conf="$(ctx::wgctl)/wgctl.conf" [[ -f "$legacy_conf" ]] && config::_load_legacy "$legacy_conf" fi # Recompute derived values after overrides _WG_CONFIG="$(ctx::wg)/${_WG_INTERFACE}.conf" _WG_TUNNEL_SPLIT="${_WG_SUBNET}, ${_WG_LAN}" } function config::_load_json() { local file="$1" [[ ! -f "$file" ]] && return 0 while IFS='=' read -r key value; do [[ -z "$key" ]] && continue 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_HANDSHAKE_CHECK_TIME_SEC="$value" ;; DATE_FORMAT) _FMT_DATE_FORMAT="$value" fmt::set_date_format "$value" ;; ACTIVITY_TOTAL_LOW_BYTES) _ACTIVITY_TOTAL_LOW_BYTES="$value" ;; ACTIVITY_TOTAL_MED_BYTES) _ACTIVITY_TOTAL_MED_BYTES="$value" ;; ACTIVITY_TOTAL_HIGH_BYTES) _ACTIVITY_TOTAL_HIGH_BYTES="$value" ;; ACTIVITY_CURRENT_LOW_BYTES) _ACTIVITY_CURRENT_LOW_BYTES="$value" ;; ACTIVITY_CURRENT_MED_BYTES) _ACTIVITY_CURRENT_MED_BYTES="$value" ;; ACTIVITY_CURRENT_HIGH_BYTES) _ACTIVITY_CURRENT_HIGH_BYTES="$value" ;; esac done < <(json::config_load "$file" 2>/dev/null) } function config::_load_legacy() { local conf_file="$1" log::wg_warning "Using legacy wgctl.conf — run 'wgctl config migrate' to upgrade" 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_HANDSHAKE_CHECK_TIME_SEC="$value" ;; DATE_FORMAT) _FMT_DATE_FORMAT="$value" fmt::set_date_format "$value" ;; esac done < "$conf_file" } # ============================================ # Validation (unchanged) # ============================================ function config::validate() { local errors=() if [[ ! -f "$_WG_SERVER_PUBLIC_KEY_FILE" ]]; then errors+=("Server public key not found: ${_WG_SERVER_PUBLIC_KEY_FILE}") fi if [[ ! -f "$_WG_SERVER_PRIVATE_KEY_FILE" ]]; then errors+=("Server private key not found: ${_WG_SERVER_PRIVATE_KEY_FILE}") fi if [[ ! -f "$_WG_CONFIG" ]]; then errors+=("WireGuard config not found: ${_WG_CONFIG}") fi local endpoint endpoint=$(config::endpoint) if [[ -z "$endpoint" ]]; then errors+=("WG_ENDPOINT is not set — required for client config generation") elif [[ "$endpoint" != *:* ]]; then errors+=("WG_ENDPOINT must include port (e.g. wg.example.com:51820)") fi local port port=$(config::port) if [[ -z "$port" ]]; then errors+=("WG_PORT is not set") elif ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then errors+=("WG_PORT must be a valid port number (1-65535)") fi local dns dns=$(config::dns) if [[ -z "$dns" ]]; then errors+=("WG_DNS is not set — required for client configs") elif ! ip::is_valid "$dns"; then errors+=("WG_DNS must be a valid IP address") fi local subnet subnet=$(config::subnet) if [[ -z "$subnet" ]]; then errors+=("WG_SUBNET is not set — required for IP allocation") fi local lan lan=$(config::lan) if [[ -z "$lan" ]]; then log::wg_warning "WG_LAN is not set — some rule features may not work correctly" fi if [[ ${#errors[@]} -gt 0 ]]; then log::error "wgctl configuration errors:" for err in "${errors[@]}"; do printf " ✗ %s\n" "$err" >&2 done printf "\n Edit %s to fix these issues.\n\n" "$(ctx::config_file)" >&2 return 1 fi return 0 } # ============================================ # Accessors (unchanged) # ============================================ function config::interface() { echo "$_WG_INTERFACE"; } function config::config_file() { echo "$_WG_CONFIG"; } function config::endpoint() { echo "$_WG_ENDPOINT"; } function config::dns() { echo "$_WG_DNS"; } function config::dns_fallback() { echo "${_WG_DNS_FALLBACK:-}"; } function config::port() { echo "$_WG_PORT"; } function config::subnet() { echo "$_WG_SUBNET"; } function config::lan() { echo "$_WG_LAN"; } function config::tunnel_split() { echo "$_WG_TUNNEL_SPLIT"; } function config::tunnel_full() { echo "$_WG_TUNNEL_FULL"; } function config::handshake_time_sec() { echo "$_WG_HANDSHAKE_CHECK_TIME_SEC"; } function config::activity_total_low() { echo "$_ACTIVITY_TOTAL_LOW_BYTES"; } function config::activity_total_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; } function config::activity_total_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; } function config::activity_current_low() { echo "$_ACTIVITY_CURRENT_LOW_BYTES"; } function config::activity_current_med() { echo "$_ACTIVITY_CURRENT_MED_BYTES"; } function config::activity_current_high() { echo "$_ACTIVITY_CURRENT_HIGH_BYTES"; } function config::server_public_key() { cat "$_WG_SERVER_PUBLIC_KEY_FILE"; } function config::allowed_ips_for() { local tunnel="${1:-split}" case "$tunnel" in full) echo "$_WG_TUNNEL_FULL" ;; split) echo "$_WG_TUNNEL_SPLIT" ;; *) log::error "Unknown tunnel mode: ${tunnel} (use 'split' or 'full')" return 1 ;; esac } function config::dns_string() { local fallback fallback=$(config::dns_fallback) if [[ -n "$fallback" ]]; then echo "$(config::dns), ${fallback}" else echo "$(config::dns)" fi }