#!/usr/bin/env bash # ============================================ # Lifecycle # ============================================ function config::on_load() { config::_init_defaults config::load config::validate fmt::set_date_format "${_FMT_DATE_FORMAT:-iso}" } # ============================================ # Defaults # ============================================ # Activity thresholds 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="${WG_INTERFACE:-wg0}" _WG_DNS="${WG_DNS:-10.0.0.103}" _WG_LAN="${WG_LAN:-10.0.0.0/24}" _WG_SUBNET="${WG_SUBNET:-10.1.0.0/16}" _WG_PORT="${WG_PORT:-51820}" _WG_ENDPOINT="${WG_ENDPOINT:-}" _WG_HANDSHAKE_CHECK_TIME_SEC="${WG_HANDSHAKE_CHECK_TIME_SEC:-180}" # 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" } # ============================================ # Validation # ============================================ function config::validate() { local errors=() # Required fields 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_LISTEN_PORT is not set") elif ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then errors+=("WG_LISTEN_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 interface interface=$(config::interface) if [[ -z "$interface" ]]; then errors+=("WG_INTERFACE is not set, defaulting to wg0") fi # Warn-only fields 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 /etc/wireguard/.wgctl/wgctl.conf to fix these issues.\n\n" >&2 return 1 fi return 0 } # ============================================ # Load overrides from .wgctl/wgctl.conf # ============================================ function config::load() { local conf_file conf_file="$(ctx::data)/wgctl.conf" [[ ! -f "$conf_file" ]] && return 0 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_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" ;; ACTIVITY_LOW_BYTES) _ACTIVITY_LOW_BYTES="$value" ;; ACTIVITY_MED_BYTES) _ACTIVITY_MED_BYTES="$value" ;; ACTIVITY_HIGH_BYTES) _ACTIVITY_HIGH_BYTES="$value" ;; DATE_FORMAT) _FMT_DATE_FORMAT="$value" fmt::set_date_format "$value" ;; esac done < "$conf_file" # Recompute derived values after overrides _WG_CONFIG="$(ctx::wg)/${_WG_INTERFACE}.conf" _WG_TUNNEL_SPLIT="${_WG_SUBNET}, ${_WG_LAN}" } # ============================================ # Device Type → Subnet Mapping # ============================================ declare -gA DEVICE_SUBNETS=( [desktop]="10.1.1" [laptop]="10.1.2" [phone]="10.1.3" [tablet]="10.1.4" [guest]="10.1.100" [guest-desktop]="10.1.101" [guest-laptop]="10.1.102" [guest-phone]="10.1.103" [guest-tablet]="10.1.104" ) # ============================================ # Tunnel Modes # ============================================ declare -gA DEVICE_TUNNEL_MODE=( [desktop]="split" [laptop]="split" [phone]="split" [tablet]="split" [guest]="split" [guest-desktop]="split" [guest-laptop]="split" [guest-phone]="split" [guest-tablet]="split" ) # ============================================ # Accessors # ============================================ 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::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_TOTAL_LOW_BYTES"; } function config::activity_current_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; } function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; } function config::server_public_key() { cat "$_WG_SERVER_PUBLIC_KEY_FILE"; } function config::device_types() { local types { set +u; types="${!DEVICE_SUBNETS[@]}"; set -u; } echo "$types" } function config::is_valid_type() { local type="$1" local subnet subnet=$(config::subnet_for "$type") [[ -n "$subnet" ]] } function config::is_guest_type() { local type="$1" [[ "$type" == "guest" || "$type" == guest-* ]] } function config::subnet_for() { local type="$1" local result { set +u; result="${DEVICE_SUBNETS[$type]:-}"; set -u; } echo "$result" } function config::default_tunnel_for() { local type="$1" local result { set +u; result="${DEVICE_TUNNEL_MODE[$type]:-split}"; set -u; } echo "$result" } function config::allowed_ips_for() { local type="$1" local tunnel="${2:-}" if [[ -z "$tunnel" ]]; then tunnel=$(config::default_tunnel_for "$type") fi 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 } # ============================================ # Validation # ============================================ function config::validate() { if [[ ! -f "$_WG_SERVER_PUBLIC_KEY_FILE" ]]; then log::error "Server public key not found: ${_WG_SERVER_PUBLIC_KEY_FILE}" exit 1 fi if [[ ! -f "$_WG_SERVER_PRIVATE_KEY_FILE" ]]; then log::error "Server private key not found: ${_WG_SERVER_PRIVATE_KEY_FILE}" exit 1 fi if [[ ! -f "$_WG_CONFIG" ]]; then log::error "WireGuard config not found: ${_WG_CONFIG}" exit 1 fi }