Остання активність 1 month ago

nuno ревизій цього gist 1 month ago. До ревизії

1 file changed, 341 insertions

gistfile1.txt(файл створено)

@@ -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 + }
Новіше Пізніше