362 lines
10 KiB
Bash
362 lines
10 KiB
Bash
#!/usr/bin/env bash
|
|
# subnet.module.sh — subnet map lookups, resolution, validation, and CIDR utilities
|
|
# All subnet data lives in subnets.json; this module wraps json:: calls
|
|
# and provides hardcoded fallbacks for safety.
|
|
|
|
# ======================================================
|
|
# CIDR Utilities
|
|
# Pure functions — no external dependencies, no side effects.
|
|
# Suitable for unit testing.
|
|
# ======================================================
|
|
|
|
# subnet::prefix <cidr>
|
|
# Returns the first three octets of a CIDR (the allocation prefix).
|
|
# Example: 10.1.3.0/24 -> 10.1.3
|
|
function subnet::prefix() {
|
|
echo "${1%%/*}" | cut -d'.' -f1-3
|
|
}
|
|
|
|
# subnet::base_ip <cidr>
|
|
# Returns the network address without the mask.
|
|
# Example: 10.1.3.0/24 -> 10.1.3.0
|
|
function subnet::base_ip() {
|
|
echo "${1%%/*}"
|
|
}
|
|
|
|
# subnet::mask <cidr>
|
|
# Returns the prefix length.
|
|
# Example: 10.1.3.0/24 -> 24
|
|
function subnet::mask() {
|
|
echo "${1##*/}"
|
|
}
|
|
|
|
# subnet::host_range <cidr>
|
|
# Returns the iterable host range for a subnet.
|
|
# Currently supports /24 (1-254). Mask-aware implementation deferred.
|
|
function subnet::host_range() {
|
|
local cidr="${1:-}"
|
|
local mask
|
|
mask=$(subnet::mask "$cidr")
|
|
case "$mask" in
|
|
24) seq 1 254 ;;
|
|
*)
|
|
# Fallback for non-/24 — still seq 1 254, but noted for future upgrade
|
|
seq 1 254
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# subnet::contains <cidr> <ip>
|
|
# Returns 0 if the IP falls within the CIDR, 1 otherwise.
|
|
# Example: subnet::contains 10.1.3.0/24 10.1.3.5 -> 0 (true)
|
|
function subnet::contains() {
|
|
local cidr="${1:-}" ip="${2:-}"
|
|
[[ -z "$cidr" || -z "$ip" ]] && return 1
|
|
|
|
local prefix mask
|
|
prefix=$(subnet::prefix "$cidr")
|
|
mask=$(subnet::mask "$cidr")
|
|
|
|
# For /24: check that the first three octets match
|
|
case "$mask" in
|
|
24)
|
|
local ip_prefix
|
|
ip_prefix=$(echo "$ip" | cut -d'.' -f1-3)
|
|
[[ "$ip_prefix" == "$prefix" ]]
|
|
;;
|
|
*)
|
|
# Delegate to Python for non-/24 subnets
|
|
python3 -c "
|
|
import ipaddress, sys
|
|
try:
|
|
net = ipaddress.ip_network('${cidr}', strict=False)
|
|
addr = ipaddress.ip_address('${ip}')
|
|
sys.exit(0 if addr in net else 1)
|
|
except Exception:
|
|
sys.exit(1)
|
|
"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# subnet::is_valid_cidr <cidr>
|
|
# Returns 0 if the string is a valid CIDR notation.
|
|
function subnet::is_valid_cidr() {
|
|
local cidr="${1:-}"
|
|
[[ "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || return 1
|
|
# Also validate each octet is 0-255
|
|
local ip
|
|
ip=$(subnet::base_ip "$cidr")
|
|
local IFS='.'
|
|
read -ra octets <<< "$ip"
|
|
for octet in "${octets[@]}"; do
|
|
(( octet >= 0 && octet <= 255 )) || return 1
|
|
done
|
|
return 0
|
|
}
|
|
|
|
# subnet::ip_valid_for <cidr> <ip>
|
|
# Returns 0 if the IP is a valid host address within the given CIDR.
|
|
# Excludes network address (.0) and broadcast address (.255) for /24.
|
|
function subnet::ip_valid_for() {
|
|
local cidr="${1:-}" ip="${2:-}"
|
|
[[ -z "$cidr" || -z "$ip" ]] && return 1
|
|
|
|
# Must be a valid IP first
|
|
ip::is_valid "$ip" || return 1
|
|
|
|
# Must be within the subnet
|
|
subnet::contains "$cidr" "$ip" || return 1
|
|
|
|
# Must not be network or broadcast address (for /24)
|
|
local mask
|
|
mask=$(subnet::mask "$cidr")
|
|
if [[ "$mask" == "24" ]]; then
|
|
local last_octet
|
|
last_octet=$(echo "$ip" | cut -d'.' -f4)
|
|
[[ "$last_octet" == "0" || "$last_octet" == "255" ]] && return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# subnet::require_valid_cidr <cidr>
|
|
# Errors and exits if the CIDR is not valid.
|
|
function subnet::require_valid_cidr() {
|
|
local cidr="${1:-}"
|
|
if ! subnet::is_valid_cidr "$cidr"; then
|
|
log::error "Invalid CIDR notation: '${cidr}'"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# subnet::require_ip_valid_for <cidr> <ip>
|
|
# Errors and exits if the IP is not a valid host for the subnet.
|
|
function subnet::require_ip_valid_for() {
|
|
local cidr="${1:-}" ip="${2:-}"
|
|
if ! subnet::ip_valid_for "$cidr" "$ip"; then
|
|
log::error "IP '${ip}' is not a valid host address in subnet '${cidr}'"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ======================================================
|
|
# Hardcoded Fallbacks
|
|
# Mirror of production subnets.json.
|
|
# Used only when subnets.json lookup fails.
|
|
# ======================================================
|
|
|
|
function subnet::_hardcoded_cidr() {
|
|
local type="${1:-}" subnet_name="${2:-}"
|
|
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" ;;
|
|
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() {
|
|
echo "split"
|
|
}
|
|
|
|
# ======================================================
|
|
# Core Resolution
|
|
# ======================================================
|
|
|
|
function subnet::policy() {
|
|
local subnet_name="${1:-}" type_key="${2:-}"
|
|
policy::for_subnet "$subnet_name" "$type_key"
|
|
}
|
|
|
|
# subnet::lookup <subnet_name> [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_cidr "" "$subnet_name"
|
|
}
|
|
|
|
# subnet::resolve_for_add <type> [subnet_name]
|
|
# Main entry point for wgctl add — returns the CIDR to allocate from.
|
|
function subnet::resolve_for_add() {
|
|
local peer_type="${1:-}" subnet_name="${2:-}"
|
|
local result
|
|
|
|
if [[ -n "$subnet_name" ]]; then
|
|
# Group entry: try type-specific child first, then "none" slot
|
|
if [[ -n "$peer_type" ]]; then
|
|
result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$peer_type" 2>/dev/null) || true
|
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
|
fi
|
|
result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" 2>/dev/null) || true
|
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
|
subnet::_hardcoded_cidr "" "$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
|
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
|
fi
|
|
|
|
subnet::_hardcoded_cidr "$peer_type"
|
|
}
|
|
|
|
# subnet::type_for_add <type_flag> [subnet_name]
|
|
# Returns the canonical type string to store in meta.
|
|
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
|
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
|
fi
|
|
|
|
echo "${type_flag:-none}"
|
|
}
|
|
|
|
# subnet::tunnel_mode <subnet_name> [type_key]
|
|
# Returns "split" or "full" for the given subnet.
|
|
function subnet::tunnel_mode() {
|
|
local subnet_name="${1:-}" type_key="${2:-}"
|
|
local policy_name
|
|
policy_name=$(policy::for_subnet "$subnet_name" "$type_key")
|
|
policy::tunnel_mode "$policy_name"
|
|
}
|
|
|
|
function subnet::type_from_ip() {
|
|
local ip="${1:-}"
|
|
[[ -z "$ip" ]] && echo "unknown" && return 0
|
|
|
|
# Fast path: hardcoded map covers all production subnets — pure bash, no subshell
|
|
local type
|
|
type=$(subnet::_hardcoded_type "$ip")
|
|
if [[ "$type" != "unknown" ]]; then
|
|
echo "$type"
|
|
return 0
|
|
fi
|
|
|
|
# Slow path: Python lookup for dynamically-added subnets not in hardcoded map
|
|
local result
|
|
result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true
|
|
if [[ -n "$result" ]]; then
|
|
echo "${result##*|}"
|
|
return 0
|
|
fi
|
|
|
|
echo "unknown"
|
|
}
|
|
|
|
# subnet::name_from_ip <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
|
|
[[ -n "$result" ]] && { echo "${result%%|*}"; return 0; }
|
|
echo ""
|
|
}
|
|
|
|
# Returns the default rule for a subnet, or empty string if none configured.
|
|
# Called by add.command.sh to resolve the rule before falling back to "user".
|
|
function subnet::default_rule() {
|
|
local subnet_name="${1:-}" type_key="${2:-}"
|
|
[[ -z "$subnet_name" ]] && return 0
|
|
local policy_name
|
|
policy_name=$(policy::for_subnet "$subnet_name" "$type_key")
|
|
policy::default_rule "$policy_name"
|
|
}
|
|
|
|
# subnet::list_names
|
|
# Returns all top-level subnet names, one per line.
|
|
# Used by commands to dynamically register --<subnet> flags.
|
|
function subnet::list_names() {
|
|
json::subnet_list_names "$(ctx::subnets)" 2>/dev/null || true
|
|
}
|
|
|
|
# ======================================================
|
|
# Validation
|
|
# ======================================================
|
|
|
|
function subnet::exists() {
|
|
local name="${1:-}"
|
|
json::subnet_exists "$(ctx::subnets)" "$name" 2>/dev/null
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 Data
|
|
# ======================================================
|
|
|
|
function subnet::list_data() {
|
|
json::subnet_list "$(ctx::subnets)" 2>/dev/null || true
|
|
}
|
|
|
|
function subnet::show_data() {
|
|
local name="${1:-}"
|
|
json::subnet_show "$(ctx::subnets)" "$name"
|
|
}
|