#!/usr/bin/env bash # subnet.module.sh — subnet map lookups, resolution, and validation # All subnet data lives in subnets.json; this module wraps json:: calls # and provides hardcoded fallbacks for safety. # =========================================================================== # Hardcoded fallbacks (mirrors production subnets.json) # Used when subnets.json lookup fails — keeps existing peers working. # These match the legacy DEVICE_SUBNETS / DEVICE_TUNNEL_MODE maps in config.module.sh. # =========================================================================== function subnet::_hardcoded_subnet() { local type="${1:-}" subnet_name="${2:-}" # If a subnet name was given, try legacy guest-* mapping first if [[ -n "$subnet_name" ]]; then case "$subnet_name" in guests) echo "10.1.100.0/24" ;; servers) echo "10.1.200.0/24" ;; iot) echo "10.1.210.0/24" ;; *) echo "10.1.0.0/24" ;; esac return 0 fi case "$type" in desktop) echo "10.1.1.0/24" ;; laptop) echo "10.1.2.0/24" ;; phone) echo "10.1.3.0/24" ;; tablet) echo "10.1.4.0/24" ;; # Legacy guest-* types — kept for existing peers during migration guest) echo "10.1.100.0/24" ;; guest-desktop) echo "10.1.101.0/24" ;; guest-laptop) echo "10.1.102.0/24" ;; guest-phone) echo "10.1.103.0/24" ;; guest-tablet) echo "10.1.104.0/24" ;; server) echo "10.1.200.0/24" ;; iot) echo "10.1.210.0/24" ;; *) echo "10.1.0.0/24" ;; esac } function subnet::_hardcoded_type() { local ip="${1:-}" case "$ip" in 10.1.1.*) echo "desktop" ;; 10.1.2.*) echo "laptop" ;; 10.1.3.*) echo "phone" ;; 10.1.4.*) echo "tablet" ;; 10.1.100.*) echo "none" ;; 10.1.101.*) echo "desktop" ;; 10.1.102.*) echo "laptop" ;; 10.1.103.*) echo "phone" ;; 10.1.104.*) echo "tablet" ;; 10.1.200.*) echo "server" ;; 10.1.210.*) echo "iot" ;; *) echo "unknown" ;; esac } function subnet::_hardcoded_tunnel_mode() { # All current subnets use split — placeholder for future full-tunnel entries echo "split" } # =========================================================================== # Core resolution # =========================================================================== # subnet::lookup [type_key] # Returns the CIDR for a given subnet name and optional type. # Falls back to hardcoded map on failure. function subnet::lookup() { local subnet_name="${1:-}" type_key="${2:-}" local result result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "$result" return 0 fi subnet::_hardcoded_subnet "" "$subnet_name" } # subnet::resolve_for_add [subnet_name] # Main entry point for wgctl add. # Returns the CIDR to allocate from. # Resolution order: # 1. subnet_name given + type given -> subnets[subnet_name][type] # 2. subnet_name given, no type -> subnets[subnet_name]["none"] # 3. no subnet_name -> subnets[type] (scalar, type-native) # 4. fallback -> hardcoded map function subnet::resolve_for_add() { local peer_type="${1:-}" subnet_name="${2:-}" local result if [[ -n "$subnet_name" ]]; then # Try with type key first if [[ -n "$peer_type" ]]; then result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$peer_type" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "$result"; return 0; fi fi # Fall back to "none" slot in group, or scalar entry result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "$result"; return 0; fi # Hardcoded fallback for subnet name subnet::_hardcoded_subnet "" "$subnet_name" return 0 fi # No subnet_name — resolve from type (native allocation) if [[ -n "$peer_type" ]]; then result=$(json::subnet_lookup "$(ctx::subnets)" "$peer_type" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "$result"; return 0; fi fi subnet::_hardcoded_subnet "$peer_type" } # subnet::type_for_add [subnet_name] # Returns the canonical type string to store in meta. # If --subnet given and it's a scalar, type comes from subnets.json entry. # If --subnet is a group, type comes from --type flag (or "none"). # If no --subnet, type comes from --type flag directly. function subnet::type_for_add() { local type_flag="${1:-}" subnet_name="${2:-}" local result if [[ -n "$subnet_name" ]]; then result=$(json::subnet_type "$(ctx::subnets)" "$subnet_name" "$type_flag" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "$result"; return 0; fi fi # No subnet or lookup failed — use the type flag directly if [[ -n "$type_flag" ]]; then echo "$type_flag" else echo "none" fi } # subnet::tunnel_mode [type_key] # Returns "split" or "full" for the given subnet. function subnet::tunnel_mode() { local subnet_name="${1:-}" type_key="${2:-}" local result result=$(json::subnet_tunnel_mode "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "$result"; return 0; fi subnet::_hardcoded_tunnel_mode } # subnet::type_from_ip # Reverse-lookup: given a peer's IP, return its type. # Tries meta file first (by peer name), then subnets.json, then hardcoded. function subnet::type_from_ip() { local ip="${1:-}" local result result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true if [[ -n "$result" ]]; then # result is "subnet_name|type_key" echo "${result##*|}" return 0 fi subnet::_hardcoded_type "$ip" } # subnet::name_from_ip # Returns the subnet name (e.g. "guests", "desktop") for an IP. function subnet::name_from_ip() { local ip="${1:-}" local result result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "${result%%|*}" return 0 fi echo "" } # =========================================================================== # Validation # =========================================================================== # subnet::exists # Returns 0 if subnet exists in subnets.json, 1 otherwise. function subnet::exists() { local name="${1:-}" json::subnet_exists "$(ctx::subnets)" "$name" 2>/dev/null } # subnet::require_exists # Errors and exits if subnet doesn't exist. function subnet::require_exists() { local name="${1:-}" if ! subnet::exists "$name"; then log::error "Subnet '${name}' not found. Use 'wgctl subnet list' to see available subnets." return 1 fi } # subnet::peers_using # Returns comma-separated list of peer names using this subnet (from meta). # Empty string if none. function subnet::peers_using() { local subnet_name="${1:-}" local peers peers=$(json::subnet_peers \ "$(ctx::meta)" \ "$(ctx::clients)" \ "$subnet_name" \ "$(ctx::subnets)" \ 2>/dev/null) || true echo "$peers" | tr '\n' ',' | sed 's/,$//' } # =========================================================================== # Display helpers # =========================================================================== # subnet::list_data # Returns all subnet entries formatted for display. # Output per line: display_name|subnet|type|tunnel_mode|desc|is_group|group_parent function subnet::list_data() { json::subnet_list "$(ctx::subnets)" 2>/dev/null || true } # subnet::show_data # Returns detail lines for a single subnet entry. function subnet::show_data() { local name="${1:-}" json::subnet_show "$(ctx::subnets)" "$name" }