Последняя активность 1 month ago

rule.command.sh Исходник
1#!/usr/bin/env bash
2
3# ============================================
4# Lifecycle
5# ============================================
6
7function cmd::rule::on_load() {
8 flag::register --name
9 flag::register --desc
10 flag::register --block-ip
11 flag::register --allow-ip
12 flag::register --block-port
13 flag::register --allow-port
14 flag::register --remove-block-ip
15 flag::register --remove-allow-ip
16 flag::register --remove-block-port
17 flag::register --remove-allow-port
18 flag::register --peer
19 flag::register --peers
20 flag::register --dns-redirect
21}
22
23# ============================================
24# Help
25# ============================================
26
27function cmd::rule::help() {
28 cat <<EOF
29Usage: wgctl rule <subcommand> [options]
30
31Manage firewall rules for peers.
32
33Subcommands:
34 list, ls List all rules
35 show Show rule details and assigned peers
36 add, new, create Create a new rule
37 update, edit Update a rule and re-apply to all peers
38 remove, rm, del Remove a rule
39 assign Assign a rule to a peer
40 unassign Remove rule from a peer
41 migrate Apply default rules to all unassigned peers
42
43Options for add/update:
44 --name <name> Rule name (e.g. guest, user, dev-01)
45 --desc <description> Human readable description
46 --allow-ip <ip/cidr> Allow IP or subnet (repeatable)
47 --allow-port <ip:port:proto> Allow specific port (repeatable)
48 --block-ip <ip/cidr> Block IP or subnet (repeatable)
49 --block-port <ip:port:proto> Block specific port (repeatable)
50 --dns-redirect Force DNS through Pi-hole
51 --remove-allow-ip <ip> Remove allow IP entry (update only)
52 --remove-allow-port <entry> Remove allow port entry (update only)
53 --remove-block-ip <ip> Remove block IP entry (update only)
54 --remove-block-port <entry> Remove block port entry (update only)
55
56Options for assign/unassign:
57 --name <rule> Rule name
58 --peer <peer> Peer name (e.g. phone-nuno)
59 --type <type> Peer device type (optional)
60
61Examples:
62 wgctl rule list
63 wgctl rule show --name guest
64 wgctl rule add --name dev-01 --desc "Dev VM only" --allow-ip 10.0.0.50 --block-ip 10.0.0.0/24
65 wgctl rule update --name user --block-port 10.0.0.100:8006:tcp
66 wgctl rule update --name user --remove-block-ip 10.0.0.99
67 wgctl rule assign --name dev-01 --peer laptop-nuno
68 wgctl rule unassign --peer laptop-nuno --type laptop
69 wgctl rule migrate
70EOF
71}
72
73# ============================================
74# Run
75# ============================================
76
77function cmd::rule::run() {
78 local subcmd="${1:-help}"
79 shift || true
80
81 case "$subcmd" in
82 list|ls) cmd::rule::list "$@" ;;
83 show) cmd::rule::show "$@" ;;
84 add|new|create) cmd::rule::add "$@" ;;
85 update|edit) cmd::rule::update "$@" ;;
86 remove|rm|del|delete) cmd::rule::remove "$@" ;;
87 assign) cmd::rule::assign "$@" ;;
88 unassign) cmd::rule::unassign "$@" ;;
89 migrate) cmd::rule::migrate "$@" ;;
90 help) cmd::rule::help ;;
91 *)
92 log::error "Unknown subcommand: '${subcmd}'"
93 cmd::rule::help
94 return 1
95 ;;
96 esac
97}
98
99# ============================================
100# List
101# ============================================
102
103function cmd::rule::list() {
104 local rules_dir
105 rules_dir="$(ctx::rules)"
106
107 local rules=("${rules_dir}"/*.rule)
108 if [[ ! -f "${rules[0]}" ]]; then
109 log::wg "No rules configured"
110 return 0
111 fi
112
113 log::section "Firewall Rules"
114 printf "\n %-20s %-45s %-8s %-8s %s\n" \
115 "NAME" "DESCRIPTION" "ALLOWS" "BLOCKS" "PEERS"
116 printf " %s\n" "$(printf '─%.0s' {1..95})"
117
118 while IFS="|" read -r name desc n_allows n_blocks peer_count; do
119 [[ -z "$name" ]] && continue
120 local short_desc="${desc:0:43}"
121 [[ ${#desc} -gt 43 ]] && short_desc="${short_desc}..."
122 printf " %-20s %-45s %-8s %-8s %s\n" \
123 "$name" "${short_desc:-}" "$n_allows" "$n_blocks" "${peer_count} peers"
124 done < <(json::rule_list_data "$rules_dir" "$(ctx::meta)")
125
126 printf "\n"
127}
128
129# ============================================
130# Show
131# ============================================
132
133function cmd::rule::show() {
134 local name="" show_peers=false
135
136 while [[ $# -gt 0 ]]; do
137 case "$1" in
138 --name) name="$2"; shift 2 ;;
139 --peers) show_peers=true; shift ;;
140 --help) cmd::rule::help; return ;;
141 *) log::error "Unknown flag: $1"; return 1 ;;
142 esac
143 done
144
145 if [[ -z "$name" ]]; then
146 log::error "Missing required flag: --name"
147 return 1
148 fi
149
150 rule::require_exists "$name" || return 1
151
152 local rule_file
153 rule_file="$(ctx::rule::path "${name}.rule")"
154
155 log::section "Rule: ${name}"
156
157 local desc dns_redirect
158 desc=$(json::get "$rule_file" "desc")
159 dns_redirect=$(json::get "$rule_file" "dns_redirect")
160
161 printf "\n %-20s %s\n" "Description:" "${desc:-}"
162 printf " %-20s %s\n" "DNS Redirect:" "${dns_redirect:-false}"
163
164 # Allow ports
165 local allow_ports
166 allow_ports=$(json::get "$rule_file" "allow_ports")
167 if [[ -n "$allow_ports" ]]; then
168 printf "\n Allow Ports:\n"
169 while IFS= read -r entry; do
170 printf " + %s\n" "$entry"
171 done <<< "$allow_ports"
172 fi
173
174 # Allow IPs
175 local allow_ips
176 allow_ips=$(json::get "$rule_file" "allow_ips")
177 if [[ -n "$allow_ips" ]]; then
178 printf "\n Allow IPs:\n"
179 while IFS= read -r ip; do
180 printf " + %s\n" "$ip"
181 done <<< "$allow_ips"
182 fi
183
184 # Block IPs
185 local block_ips
186 block_ips=$(json::get "$rule_file" "block_ips")
187 if [[ -n "$block_ips" ]]; then
188 printf "\n Block IPs:\n"
189 while IFS= read -r ip; do
190 printf " - %s\n" "$ip"
191 done <<< "$block_ips"
192 fi
193
194 # Block ports
195 local block_ports
196 block_ports=$(json::get "$rule_file" "block_ports")
197 if [[ -n "$block_ports" ]]; then
198 printf "\n Block Ports:\n"
199 while IFS= read -r entry; do
200 printf " - %s\n" "$entry"
201 done <<< "$block_ports"
202 fi
203
204 # Precompute peers before any other operations
205 local peer_list=()
206 mapfile -t peer_list < <(peers::with_rule "$name")
207 local peer_count=${#peer_list[@]}
208
209 # Peer count — always shown
210 printf "\n %-20s %s\n" "Assigned Peers:" "$peer_count"
211 printf " %s\n" "$(printf '─%.0s' {1..40})"
212
213 # Peer details — only with --peers flag
214 if $show_peers && [[ $peer_count -gt 0 ]]; then
215 for peer_name in "${peer_list[@]}"; do
216 local ip
217 ip=$(peers::get_ip "$peer_name")
218 printf " %-28s %s\n" "$peer_name" "$ip"
219 done
220 fi
221
222 printf "\n"
223}
224
225# ============================================
226# Add
227# ============================================
228
229function cmd::rule::add() {
230 local name="" desc=""
231 local allow_ips=() block_ips=() block_ports=()
232 local dns_redirect=false
233
234 while [[ $# -gt 0 ]]; do
235 case "$1" in
236 --name) name="$2"; shift 2 ;;
237 --desc) desc="$2"; shift 2 ;;
238 --allow-ip) allow_ips+=("$2"); shift 2 ;;
239 --block-ip) block_ips+=("$2"); shift 2 ;;
240 --block-port) block_ports+=("$2"); shift 2 ;;
241 --dns-redirect) dns_redirect=true; shift ;;
242 --help) cmd::rule::help; return ;;
243 *)
244 log::error "Unknown flag: $1"
245 return 1
246 ;;
247 esac
248 done
249
250 if [[ -z "$name" ]]; then
251 log::error "Missing required flag: --name"
252 return 1
253 fi
254
255 if rule::exists "$name"; then
256 log::error "Rule already exists: ${name}"
257 return 1
258 fi
259
260 local rule_file
261 rule_file="$(ctx::rule::path "${name}.rule")"
262
263 # Build JSON using json_helper
264 python3 -c "
265import json
266rule = {
267 'name': '${name}',
268 'desc': '${desc}',
269 'dns_redirect': $(${dns_redirect} && echo 'true' || echo 'false'),
270 'allow_ips': [$(printf '"%s",' "${allow_ips[@]}" | sed 's/,$//')] ,
271 'block_ips': [$(printf '"%s",' "${block_ips[@]}" | sed 's/,$//')],
272 'block_ports': [$(printf '"%s",' "${block_ports[@]}" | sed 's/,$//')]
273}
274with open('${rule_file}', 'w') as f:
275 json.dump(rule, f, indent=2)
276"
277
278 log::wg_success "Rule created: ${name}"
279}
280
281# ============================================
282# Update
283# ============================================
284
285function cmd::rule::update() {
286 local name="" desc=""
287 local allow_ips=() block_ips=() block_ports=()
288 local allow_ports=()
289 local rm_allow_ips=() rm_block_ips=() rm_block_ports=() rm_allow_ports=()
290 local dns_redirect=""
291
292 while [[ $# -gt 0 ]]; do
293 case "$1" in
294 --name) name="$2"; shift 2 ;;
295 --desc) desc="$2"; shift 2 ;;
296 --allow-ip) allow_ips+=("$2"); shift 2 ;;
297 --block-ip) block_ips+=("$2"); shift 2 ;;
298 --block-port) block_ports+=("$2"); shift 2 ;;
299 --allow-port) allow_ports+=("$2"); shift 2 ;;
300 --remove-allow-ip) rm_allow_ips+=("$2"); shift 2 ;;
301 --remove-block-ip) rm_block_ips+=("$2"); shift 2 ;;
302 --remove-block-port) rm_block_ports+=("$2"); shift 2 ;;
303 --remove-allow-port) rm_allow_ports+=("$2"); shift 2 ;;
304 --dns-redirect) dns_redirect=true; shift ;;
305 --help) cmd::rule::help; return ;;
306 *)
307 log::error "Unknown flag: $1"
308 return 1
309 ;;
310 esac
311 done
312
313 if [[ -z "$name" ]]; then
314 log::error "Missing required flag: --name"
315 return 1
316 fi
317
318 rule::require_exists "$name" || return 1
319
320 local rule_file
321 rule_file="$(ctx::rule::path "${name}.rule")"
322
323 # Update desc and dns_redirect
324 [[ -n "$desc" ]] && json::set "$rule_file" "desc" "\"$desc\""
325 [[ -n "$dns_redirect" ]] && json::set "$rule_file" "dns_redirect" "true"
326
327 # Add entries
328 for ip in "${allow_ips[@]}"; do json::append "$rule_file" "allow_ips" "$ip"; done
329 for ip in "${block_ips[@]}"; do json::append "$rule_file" "block_ips" "$ip"; done
330 for p in "${block_ports[@]}"; do json::append "$rule_file" "block_ports" "$p"; done
331 for p in "${allow_ports[@]}"; do json::append "$rule_file" "allow_ports" "$p"; done
332
333 # Remove entries
334 for ip in "${rm_allow_ips[@]}"; do json::remove "$rule_file" "allow_ips" "$ip"; done
335 for ip in "${rm_block_ips[@]}"; do json::remove "$rule_file" "block_ips" "$ip"; done
336 for p in "${rm_block_ports[@]}"; do json::remove "$rule_file" "block_ports" "$p"; done
337 for p in "${rm_allow_ports[@]}"; do json::remove "$rule_file" "allow_ports" "$p"; done
338
339 log::wg_success "Rule updated: ${name}"
340
341 # Re-apply to all assigned peers
342 rule::reapply_all "$name"
343}
344
345# ============================================
346# Remove
347# ============================================
348
349function cmd::rule::remove() {
350 local name="" force=false
351
352 while [[ $# -gt 0 ]]; do
353 case "$1" in
354 --name) name="$2"; shift 2 ;;
355 --force) force=true; shift ;;
356 --help) cmd::rule::help; return ;;
357 *)
358 log::error "Unknown flag: $1"
359 return 1
360 ;;
361 esac
362 done
363
364 if [[ -z "$name" ]]; then
365 log::error "Missing required flag: --name"
366 return 1
367 fi
368
369 rule::require_exists "$name" || return 1
370
371 # Check for assigned peers
372 local peer_count
373 peer_count=$(peers::with_rule "$name" | grep -c . || echo 0)
374 if [[ "$peer_count" -gt 0 ]]; then
375 log::error "Rule '${name}' is assigned to ${peer_count} peer(s) — unassign first or use --force"
376 if ! $force; then
377 return 1
378 fi
379 # Force: unassign from all peers
380 while IFS= read -r peer; do
381 local ip
382 ip=$(peers::get_ip "$peer")
383 rule::unapply "$name" "$ip"
384 done < <(peers::with_rule "$name")
385 fi
386
387 rm -f "$(ctx::rule::path "${name}.rule")"
388 log::wg_success "Rule removed: ${name}"
389}
390
391# ============================================
392# Assign
393# ============================================
394
395function cmd::rule::assign() {
396 local name="" peer="" type=""
397
398 while [[ $# -gt 0 ]]; do
399 case "$1" in
400 --name) name="$2"; shift 2 ;;
401 --peer) peer="$2"; shift 2 ;;
402 --type) type="$2"; shift 2 ;;
403 --help) cmd::rule::help; return ;;
404 *)
405 log::error "Unknown flag: $1"
406 return 1
407 ;;
408 esac
409 done
410
411 if [[ -z "$name" || -z "$peer" ]]; then
412 log::error "Missing required flags: --name and --peer"
413 return 1
414 fi
415
416 rule::require_exists "$name" || return 1
417
418 # Support --type for peer resolution
419 peer=$(peers::resolve_and_require "$peer" "$type") || return 1
420
421 # Unapply existing rule first if any
422 local existing_rule
423 existing_rule=$(peers::get_meta "$peer" "rule")
424 if [[ -n "$existing_rule" ]]; then
425 local ip
426 ip=$(peers::get_ip "$peer")
427 rule::unapply "$existing_rule" "$ip"
428 log::wg "Removed existing rule '${existing_rule}' from: ${peer}"
429 fi
430
431 local ip
432 ip=$(peers::get_ip "$peer")
433 rule::apply "$name" "$ip"
434
435 log::wg_success "Assigned rule '${name}' to: ${peer}"
436}
437
438# ============================================
439# Unassign
440# ============================================
441
442function cmd::rule::unassign() {
443 local peer="" type=""
444
445 while [[ $# -gt 0 ]]; do
446 case "$1" in
447 --peer) peer="$2"; shift 2 ;;
448 --type) type="$2"; shift 2 ;;
449 --help) cmd::rule::help; return ;;
450 *)
451 log::error "Unknown flag: $1"
452 return 1
453 ;;
454 esac
455 done
456
457 if [[ -z "$peer" ]]; then
458 log::error "Missing required flag: --peer"
459 return 1
460 fi
461
462 peer=$(peers::resolve_and_require "$peer" "$type") || return 1
463
464 local existing_rule
465 existing_rule=$(peers::get_meta "$peer" "rule")
466
467 if [[ -z "$existing_rule" ]]; then
468 log::wg_warning "Peer '${peer}' has no assigned rule"
469 return 0
470 fi
471
472 local ip
473 ip=$(peers::get_ip "$peer")
474 rule::unapply "$existing_rule" "$ip"
475
476 log::wg_success "Unassigned rule from: ${peer}"
477}
478
479# ============================================
480# Migrate Rules
481# ============================================
482
483function cmd::rule::migrate() {
484 log::section "Migrating peers to default rules"
485
486 # Write migration plan to temp file to avoid fd conflicts
487 local tmp
488 tmp=$(mktemp)
489
490 while IFS= read -r peer_name; do
491 local existing
492 existing=$(peers::get_meta "$peer_name" "rule")
493 [[ -n "$existing" ]] && continue
494 local default_rule
495 default_rule=$(peers::default_rule "$peer_name")
496 rule::exists "$default_rule" || continue
497 local ip
498 ip=$(peers::get_ip "$peer_name")
499 echo "${peer_name} ${default_rule} ${ip}" >> "$tmp"
500 done < <(peers::all)
501
502 local count=0
503 local lines
504 mapfile -t lines < "$tmp"
505 echo "DEBUG: lines count=${#lines[@]}"
506for line in "${lines[@]}"; do
507 echo "DEBUG: processing line=$line"
508 IFS=" " read -r peer_name default_rule ip <<< "$line"
509 rule::apply "$default_rule" "$ip" "$peer_name" </dev/null
510 echo "DEBUG: after apply, count=$count"
511 (( count++ )) || true
512 echo "DEBUG: incremented count=$count"
513 done
514 echo "DEBUG: loop done, count=$count"
515
516 echo "DEBUG: final count=$count"
517 echo "DEBUG: tmp contents:"
518 cat "$tmp"
519
520 rm -f "$tmp"
521 log::wg_success "Migrated ${count} peers"
522}