nuno revisou este gist 1 month ago. Ir para a revisão
1 file changed, 341 insertions
gistfile1.txt(arquivo criado)
| @@ -0,0 +1,341 @@ | |||
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # subnet.module.sh — subnet map lookups, resolution, validation, and CIDR utilities | |
| 3 | + | # All subnet data lives in subnets.json; this module wraps json:: calls | |
| 4 | + | # and provides hardcoded fallbacks for safety. | |
| 5 | + | ||
| 6 | + | # ====================================================== | |
| 7 | + | # CIDR Utilities | |
| 8 | + | # Pure functions — no external dependencies, no side effects. | |
| 9 | + | # Suitable for unit testing. | |
| 10 | + | # ====================================================== | |
| 11 | + | ||
| 12 | + | # subnet::prefix <cidr> | |
| 13 | + | # Returns the first three octets of a CIDR (the allocation prefix). | |
| 14 | + | # Example: 10.1.3.0/24 -> 10.1.3 | |
| 15 | + | function subnet::prefix() { | |
| 16 | + | echo "${1%%/*}" | cut -d'.' -f1-3 | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | # subnet::base_ip <cidr> | |
| 20 | + | # Returns the network address without the mask. | |
| 21 | + | # Example: 10.1.3.0/24 -> 10.1.3.0 | |
| 22 | + | function subnet::base_ip() { | |
| 23 | + | echo "${1%%/*}" | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | # subnet::mask <cidr> | |
| 27 | + | # Returns the prefix length. | |
| 28 | + | # Example: 10.1.3.0/24 -> 24 | |
| 29 | + | function subnet::mask() { | |
| 30 | + | echo "${1##*/}" | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | # subnet::host_range <cidr> | |
| 34 | + | # Returns the iterable host range for a subnet. | |
| 35 | + | # Currently supports /24 (1-254). Mask-aware implementation deferred. | |
| 36 | + | function subnet::host_range() { | |
| 37 | + | local cidr="${1:-}" | |
| 38 | + | local mask | |
| 39 | + | mask=$(subnet::mask "$cidr") | |
| 40 | + | case "$mask" in | |
| 41 | + | 24) seq 1 254 ;; | |
| 42 | + | *) | |
| 43 | + | # Fallback for non-/24 — still seq 1 254, but noted for future upgrade | |
| 44 | + | seq 1 254 | |
| 45 | + | ;; | |
| 46 | + | esac | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | # subnet::contains <cidr> <ip> | |
| 50 | + | # Returns 0 if the IP falls within the CIDR, 1 otherwise. | |
| 51 | + | # Example: subnet::contains 10.1.3.0/24 10.1.3.5 -> 0 (true) | |
| 52 | + | function subnet::contains() { | |
| 53 | + | local cidr="${1:-}" ip="${2:-}" | |
| 54 | + | [[ -z "$cidr" || -z "$ip" ]] && return 1 | |
| 55 | + | ||
| 56 | + | local prefix mask | |
| 57 | + | prefix=$(subnet::prefix "$cidr") | |
| 58 | + | mask=$(subnet::mask "$cidr") | |
| 59 | + | ||
| 60 | + | # For /24: check that the first three octets match | |
| 61 | + | case "$mask" in | |
| 62 | + | 24) | |
| 63 | + | local ip_prefix | |
| 64 | + | ip_prefix=$(echo "$ip" | cut -d'.' -f1-3) | |
| 65 | + | [[ "$ip_prefix" == "$prefix" ]] | |
| 66 | + | ;; | |
| 67 | + | *) | |
| 68 | + | # Delegate to Python for non-/24 subnets | |
| 69 | + | python3 -c " | |
| 70 | + | import ipaddress, sys | |
| 71 | + | try: | |
| 72 | + | net = ipaddress.ip_network('${cidr}', strict=False) | |
| 73 | + | addr = ipaddress.ip_address('${ip}') | |
| 74 | + | sys.exit(0 if addr in net else 1) | |
| 75 | + | except Exception: | |
| 76 | + | sys.exit(1) | |
| 77 | + | " | |
| 78 | + | ;; | |
| 79 | + | esac | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | # subnet::is_valid_cidr <cidr> | |
| 83 | + | # Returns 0 if the string is a valid CIDR notation. | |
| 84 | + | function subnet::is_valid_cidr() { | |
| 85 | + | local cidr="${1:-}" | |
| 86 | + | [[ "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || return 1 | |
| 87 | + | # Also validate each octet is 0-255 | |
| 88 | + | local ip | |
| 89 | + | ip=$(subnet::base_ip "$cidr") | |
| 90 | + | local IFS='.' | |
| 91 | + | read -ra octets <<< "$ip" | |
| 92 | + | for octet in "${octets[@]}"; do | |
| 93 | + | (( octet >= 0 && octet <= 255 )) || return 1 | |
| 94 | + | done | |
| 95 | + | return 0 | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | # subnet::ip_valid_for <cidr> <ip> | |
| 99 | + | # Returns 0 if the IP is a valid host address within the given CIDR. | |
| 100 | + | # Excludes network address (.0) and broadcast address (.255) for /24. | |
| 101 | + | function subnet::ip_valid_for() { | |
| 102 | + | local cidr="${1:-}" ip="${2:-}" | |
| 103 | + | [[ -z "$cidr" || -z "$ip" ]] && return 1 | |
| 104 | + | ||
| 105 | + | # Must be a valid IP first | |
| 106 | + | ip::is_valid "$ip" || return 1 | |
| 107 | + | ||
| 108 | + | # Must be within the subnet | |
| 109 | + | subnet::contains "$cidr" "$ip" || return 1 | |
| 110 | + | ||
| 111 | + | # Must not be network or broadcast address (for /24) | |
| 112 | + | local mask | |
| 113 | + | mask=$(subnet::mask "$cidr") | |
| 114 | + | if [[ "$mask" == "24" ]]; then | |
| 115 | + | local last_octet | |
| 116 | + | last_octet=$(echo "$ip" | cut -d'.' -f4) | |
| 117 | + | [[ "$last_octet" == "0" || "$last_octet" == "255" ]] && return 1 | |
| 118 | + | fi | |
| 119 | + | ||
| 120 | + | return 0 | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | # subnet::require_valid_cidr <cidr> | |
| 124 | + | # Errors and exits if the CIDR is not valid. | |
| 125 | + | function subnet::require_valid_cidr() { | |
| 126 | + | local cidr="${1:-}" | |
| 127 | + | if ! subnet::is_valid_cidr "$cidr"; then | |
| 128 | + | log::error "Invalid CIDR notation: '${cidr}'" | |
| 129 | + | return 1 | |
| 130 | + | fi | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | # subnet::require_ip_valid_for <cidr> <ip> | |
| 134 | + | # Errors and exits if the IP is not a valid host for the subnet. | |
| 135 | + | function subnet::require_ip_valid_for() { | |
| 136 | + | local cidr="${1:-}" ip="${2:-}" | |
| 137 | + | if ! subnet::ip_valid_for "$cidr" "$ip"; then | |
| 138 | + | log::error "IP '${ip}' is not a valid host address in subnet '${cidr}'" | |
| 139 | + | return 1 | |
| 140 | + | fi | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | # ====================================================== | |
| 144 | + | # Hardcoded Fallbacks | |
| 145 | + | # Mirror of production subnets.json. | |
| 146 | + | # Used only when subnets.json lookup fails. | |
| 147 | + | # ====================================================== | |
| 148 | + | ||
| 149 | + | function subnet::_hardcoded_cidr() { | |
| 150 | + | local type="${1:-}" subnet_name="${2:-}" | |
| 151 | + | if [[ -n "$subnet_name" ]]; then | |
| 152 | + | case "$subnet_name" in | |
| 153 | + | guests) echo "10.1.100.0/24" ;; | |
| 154 | + | servers) echo "10.1.200.0/24" ;; | |
| 155 | + | iot) echo "10.1.210.0/24" ;; | |
| 156 | + | *) echo "10.1.0.0/24" ;; | |
| 157 | + | esac | |
| 158 | + | return 0 | |
| 159 | + | fi | |
| 160 | + | case "$type" in | |
| 161 | + | desktop) echo "10.1.1.0/24" ;; | |
| 162 | + | laptop) echo "10.1.2.0/24" ;; | |
| 163 | + | phone) echo "10.1.3.0/24" ;; | |
| 164 | + | tablet) echo "10.1.4.0/24" ;; | |
| 165 | + | guest) echo "10.1.100.0/24" ;; | |
| 166 | + | guest-desktop) echo "10.1.101.0/24" ;; | |
| 167 | + | guest-laptop) echo "10.1.102.0/24" ;; | |
| 168 | + | guest-phone) echo "10.1.103.0/24" ;; | |
| 169 | + | guest-tablet) echo "10.1.104.0/24" ;; | |
| 170 | + | server) echo "10.1.200.0/24" ;; | |
| 171 | + | iot) echo "10.1.210.0/24" ;; | |
| 172 | + | *) echo "10.1.0.0/24" ;; | |
| 173 | + | esac | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | function subnet::_hardcoded_type() { | |
| 177 | + | local ip="${1:-}" | |
| 178 | + | case "$ip" in | |
| 179 | + | 10.1.1.*) echo "desktop" ;; | |
| 180 | + | 10.1.2.*) echo "laptop" ;; | |
| 181 | + | 10.1.3.*) echo "phone" ;; | |
| 182 | + | 10.1.4.*) echo "tablet" ;; | |
| 183 | + | 10.1.100.*) echo "none" ;; | |
| 184 | + | 10.1.101.*) echo "desktop" ;; | |
| 185 | + | 10.1.102.*) echo "laptop" ;; | |
| 186 | + | 10.1.103.*) echo "phone" ;; | |
| 187 | + | 10.1.104.*) echo "tablet" ;; | |
| 188 | + | 10.1.200.*) echo "server" ;; | |
| 189 | + | 10.1.210.*) echo "iot" ;; | |
| 190 | + | *) echo "unknown" ;; | |
| 191 | + | esac | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | function subnet::_hardcoded_tunnel_mode() { | |
| 195 | + | echo "split" | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | # ====================================================== | |
| 199 | + | # Core Resolution | |
| 200 | + | # ====================================================== | |
| 201 | + | ||
| 202 | + | # subnet::lookup <subnet_name> [type_key] | |
| 203 | + | # Returns the CIDR for a given subnet name and optional type. | |
| 204 | + | # Falls back to hardcoded map on failure. | |
| 205 | + | function subnet::lookup() { | |
| 206 | + | local subnet_name="${1:-}" type_key="${2:-}" | |
| 207 | + | local result | |
| 208 | + | result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true | |
| 209 | + | if [[ -n "$result" ]]; then | |
| 210 | + | echo "$result" | |
| 211 | + | return 0 | |
| 212 | + | fi | |
| 213 | + | subnet::_hardcoded_cidr "" "$subnet_name" | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | # subnet::resolve_for_add <type> [subnet_name] | |
| 217 | + | # Main entry point for wgctl add — returns the CIDR to allocate from. | |
| 218 | + | function subnet::resolve_for_add() { | |
| 219 | + | local peer_type="${1:-}" subnet_name="${2:-}" | |
| 220 | + | local result | |
| 221 | + | ||
| 222 | + | if [[ -n "$subnet_name" ]]; then | |
| 223 | + | # Group entry: try type-specific child first, then "none" slot | |
| 224 | + | if [[ -n "$peer_type" ]]; then | |
| 225 | + | result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$peer_type" 2>/dev/null) || true | |
| 226 | + | [[ -n "$result" ]] && { echo "$result"; return 0; } | |
| 227 | + | fi | |
| 228 | + | result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" 2>/dev/null) || true | |
| 229 | + | [[ -n "$result" ]] && { echo "$result"; return 0; } | |
| 230 | + | subnet::_hardcoded_cidr "" "$subnet_name" | |
| 231 | + | return 0 | |
| 232 | + | fi | |
| 233 | + | ||
| 234 | + | # No subnet_name — resolve from type (native allocation) | |
| 235 | + | if [[ -n "$peer_type" ]]; then | |
| 236 | + | result=$(json::subnet_lookup "$(ctx::subnets)" "$peer_type" 2>/dev/null) || true | |
| 237 | + | [[ -n "$result" ]] && { echo "$result"; return 0; } | |
| 238 | + | fi | |
| 239 | + | ||
| 240 | + | subnet::_hardcoded_cidr "$peer_type" | |
| 241 | + | } | |
| 242 | + | ||
| 243 | + | # subnet::type_for_add <type_flag> [subnet_name] | |
| 244 | + | # Returns the canonical type string to store in meta. | |
| 245 | + | function subnet::type_for_add() { | |
| 246 | + | local type_flag="${1:-}" subnet_name="${2:-}" | |
| 247 | + | local result | |
| 248 | + | ||
| 249 | + | if [[ -n "$subnet_name" ]]; then | |
| 250 | + | result=$(json::subnet_type "$(ctx::subnets)" "$subnet_name" "$type_flag" 2>/dev/null) || true | |
| 251 | + | [[ -n "$result" ]] && { echo "$result"; return 0; } | |
| 252 | + | fi | |
| 253 | + | ||
| 254 | + | echo "${type_flag:-none}" | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | # subnet::tunnel_mode <subnet_name> [type_key] | |
| 258 | + | # Returns "split" or "full" for the given subnet. | |
| 259 | + | function subnet::tunnel_mode() { | |
| 260 | + | local subnet_name="${1:-}" type_key="${2:-}" | |
| 261 | + | local result | |
| 262 | + | result=$(json::subnet_tunnel_mode "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true | |
| 263 | + | [[ -n "$result" ]] && { echo "$result"; return 0; } | |
| 264 | + | subnet::_hardcoded_tunnel_mode | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | function subnet::type_from_ip() { | |
| 268 | + | local ip="${1:-}" | |
| 269 | + | [[ -z "$ip" ]] && echo "unknown" && return 0 | |
| 270 | + | ||
| 271 | + | # Fast path: hardcoded map covers all production subnets — pure bash, no subshell | |
| 272 | + | local type | |
| 273 | + | type=$(subnet::_hardcoded_type "$ip") | |
| 274 | + | if [[ "$type" != "unknown" ]]; then | |
| 275 | + | echo "$type" | |
| 276 | + | return 0 | |
| 277 | + | fi | |
| 278 | + | ||
| 279 | + | # Slow path: Python lookup for dynamically-added subnets not in hardcoded map | |
| 280 | + | local result | |
| 281 | + | result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true | |
| 282 | + | if [[ -n "$result" ]]; then | |
| 283 | + | echo "${result##*|}" | |
| 284 | + | return 0 | |
| 285 | + | fi | |
| 286 | + | ||
| 287 | + | echo "unknown" | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | # subnet::name_from_ip <ip> | |
| 291 | + | # Returns the subnet name (e.g. "guests", "desktop") for an IP. | |
| 292 | + | function subnet::name_from_ip() { | |
| 293 | + | local ip="${1:-}" | |
| 294 | + | local result | |
| 295 | + | result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true | |
| 296 | + | [[ -n "$result" ]] && { echo "${result%%|*}"; return 0; } | |
| 297 | + | echo "" | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | # ====================================================== | |
| 301 | + | # Validation | |
| 302 | + | # ====================================================== | |
| 303 | + | ||
| 304 | + | function subnet::exists() { | |
| 305 | + | local name="${1:-}" | |
| 306 | + | json::subnet_exists "$(ctx::subnets)" "$name" 2>/dev/null | |
| 307 | + | } | |
| 308 | + | ||
| 309 | + | function subnet::require_exists() { | |
| 310 | + | local name="${1:-}" | |
| 311 | + | if ! subnet::exists "$name"; then | |
| 312 | + | log::error "Subnet '${name}' not found. Use 'wgctl subnet list' to see available subnets." | |
| 313 | + | return 1 | |
| 314 | + | fi | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | function subnet::peers_using() { | |
| 318 | + | local subnet_name="${1:-}" | |
| 319 | + | local peers | |
| 320 | + | peers=$(json::subnet_peers \ | |
| 321 | + | "$(ctx::meta)" \ | |
| 322 | + | "$(ctx::clients)" \ | |
| 323 | + | "$subnet_name" \ | |
| 324 | + | "$(ctx::subnets)" \ | |
| 325 | + | 2>/dev/null) || true | |
| 326 | + | echo "$peers" | tr '\n' ',' | sed 's/,$//' | |
| 327 | + | } | |
| 328 | + | ||
| 329 | + | ||
| 330 | + | # ====================================================== | |
| 331 | + | # Display Data | |
| 332 | + | # ====================================================== | |
| 333 | + | ||
| 334 | + | function subnet::list_data() { | |
| 335 | + | json::subnet_list "$(ctx::subnets)" 2>/dev/null || true | |
| 336 | + | } | |
| 337 | + | ||
| 338 | + | function subnet::show_data() { | |
| 339 | + | local name="${1:-}" | |
| 340 | + | json::subnet_show "$(ctx::subnets)" "$name" | |
| 341 | + | } | |
Próximo
Anterior