最終更新 1 month ago

nuno revised this gist 1 month ago. Go to revision

1 file changed, 439 insertions

rule.module.sh(file created)

@@ -0,0 +1,439 @@
1 + #!/usr/bin/env bash
2 +
3 + # ============================================
4 + # Rule File Parsing
5 + # ============================================
6 +
7 + function rule::is_base() {
8 + local name="${1:-}"
9 + [[ -f "$(ctx::rules::base)/${name}.rule" ]]
10 + }
11 +
12 + function rule::exists() {
13 + local name="${1:-}"
14 + local path
15 + path=$(json::find_rule_file "$(ctx::rules)" "$name")
16 + [[ -n "$path" ]]
17 + }
18 +
19 + function rule::require_assignable() {
20 + local name="${1:-}"
21 + if rule::is_base "$name"; then
22 + log::error "Cannot assign base rule '${name}' — base rules cannot be assigned directly"
23 + return 1
24 + fi
25 + }
26 +
27 + function rule::require_exists() {
28 + local name="${1:-}"
29 + if ! rule::exists "$name"; then
30 + log::error "Rule not found: ${name}"
31 + return 1
32 + fi
33 + }
34 +
35 + function rule::get() {
36 + local name="${1:-}" key="${2:-}"
37 + json::rule_resolve_field "$(ctx::rules)" "$name" "$key"
38 + }
39 +
40 + function rule::get_own() {
41 + local name="${1:-}" key="${2:-}"
42 + local file
43 + file=$(rule::path "$name") || return 0
44 + json::get_raw "$file" "$key"
45 + }
46 +
47 + function rule::get_resolved() {
48 + local name="${1:-}"
49 + json::rule_resolve "$(ctx::rules)" "$name"
50 + }
51 +
52 + function rule::path() {
53 + local name="${1:-}"
54 + local path
55 + path=$(json::find_rule_file "$(ctx::rules)" "$name")
56 + [[ -n "$path" ]] && echo "$path" || return 1
57 + }
58 +
59 + function rule::get_all() {
60 + local name="${1:-}"
61 + rule::get_resolved "$name"
62 + }
63 +
64 + function rule::is_applied() {
65 + local rule_name="${1:-}" client_ip="${2:-}"
66 +
67 + local first_port
68 + first_port=$(rule::get "$rule_name" "block_ports" | head -1)
69 + if [[ -n "$first_port" ]]; then
70 + local target port proto
71 + IFS=":" read -r target port proto <<< "$first_port"
72 + proto="${proto:-tcp}"
73 + fw::has_block_rule "$client_ip" "$target" "$proto" "$port"
74 + return $?
75 + fi
76 +
77 + local first_ip
78 + first_ip=$(rule::get "$rule_name" "block_ips" | head -1)
79 + if [[ -n "$first_ip" ]]; then
80 + fw::has_block_rule "$client_ip" "$first_ip"
81 + return $?
82 + fi
83 +
84 + local first_allow
85 + first_allow=$(rule::get "$rule_name" "allow_ports" | head -1)
86 + if [[ -n "$first_allow" ]]; then
87 + local target port proto
88 + IFS=":" read -r target port proto <<< "$first_allow"
89 + proto="${proto:-tcp}"
90 + fw::_forward_exists -s "$client_ip" -d "$target" \
91 + -p "$proto" --dport "$port" -j ACCEPT
92 + return $?
93 + fi
94 +
95 + return 1
96 + }
97 +
98 + # ============================================
99 + # Rule Application
100 + # ============================================
101 +
102 + function rule::apply() {
103 + local rule_name="${1:?rule_name required}"
104 + local client_ip="${2:?client_ip required}"
105 + local peer_name="${3:-}"
106 +
107 + rule::require_exists "$rule_name" || return 1
108 +
109 + if [[ -z "$peer_name" ]]; then
110 + peer_name=$(peers::find_by_ip "$client_ip")
111 + fi
112 +
113 + log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
114 +
115 + # Check if already applied
116 + if rule::is_applied "$rule_name" "$client_ip"; then
117 + log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
118 + [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
119 + return 0
120 + fi
121 +
122 + # Process block_ips
123 + while IFS= read -r block_ip; do
124 + [[ -z "$block_ip" ]] && continue
125 + fw::block_ip "$client_ip" "$block_ip"
126 + done < <(rule::get "$rule_name" "block_ips")
127 +
128 + # Process block_ports
129 + while IFS= read -r entry; do
130 + [[ -z "$entry" ]] && continue
131 + local target port proto
132 + IFS=":" read -r target port proto <<< "$entry"
133 + proto="${proto:-tcp}"
134 + fw::block_port "$client_ip" "$target" "$port" "$proto"
135 + done < <(rule::get "$rule_name" "block_ports")
136 +
137 + # Process allow_ips (inserted before blocks)
138 + while IFS= read -r allow_ip; do
139 + [[ -z "$allow_ip" ]] && continue
140 + fw::allow_ip "$client_ip" "$allow_ip"
141 + done < <(rule::get "$rule_name" "allow_ips")
142 +
143 + # Process allow_ports (highest priority)
144 + while IFS= read -r entry; do
145 + [[ -z "$entry" ]] && continue
146 + local target port proto
147 + IFS=":" read -r target port proto <<< "$entry"
148 + proto="${proto:-tcp}"
149 + fw::allow_port "$client_ip" "$target" "$port" "$proto"
150 + done < <(rule::get "$rule_name" "allow_ports")
151 +
152 + # Persist rule assignment
153 + [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
154 +
155 + # DNS redirect
156 + local dns_redirect
157 + dns_redirect=$(rule::get "$rule_name" "dns_redirect")
158 + if [[ "$dns_redirect" == "true" ]]; then
159 + local peer_subnet
160 + peer_subnet=$(peers::get_ip "$peer_name" | cut -d'.' -f1-3)
161 + if ! fw::_nat_exists -i wg0 -s "${peer_subnet}.0/24" \
162 + -p udp --dport 53 -j DNAT \
163 + --to-destination "$(config::dns):53" 2>/dev/null; then
164 + rule::apply_dns_redirect "${peer_subnet}.0/24"
165 + log::debug "dns_redirect: applied for ${peer_subnet}.0/24"
166 + else
167 + log::debug "dns_redirect: already applied for ${peer_subnet}.0/24"
168 + fi
169 + fi
170 +
171 + log::debug "Applied rule '${rule_name}' to: ${client_ip}"
172 + }
173 +
174 + function rule::unapply() {
175 + local rule_name="${1:-}" client_ip="${2:-}"
176 +
177 + rule::require_exists "$rule_name" || return 1
178 +
179 + local peer_name
180 + peer_name=$(peers::find_by_ip "$client_ip")
181 +
182 + # Remove allow_ports first (reverse order of apply)
183 + while IFS= read -r entry; do
184 + [[ -z "$entry" ]] && continue
185 + local target port proto
186 + IFS=":" read -r target port proto <<< "$entry"
187 + proto="${proto:-tcp}"
188 + fw::unallow_port "$client_ip" "$target" "$port" "$proto"
189 + done < <(rule::get "$rule_name" "allow_ports")
190 +
191 + # Remove allow_ips
192 + while IFS= read -r allow_ip; do
193 + [[ -z "$allow_ip" ]] && continue
194 + if [[ "$allow_ip" == *"/"* ]]; then
195 + fw::unallow_subnet "$client_ip" "$allow_ip"
196 + else
197 + fw::unallow_ip "$client_ip" "$allow_ip"
198 + fi
199 + done < <(rule::get "$rule_name" "allow_ips")
200 +
201 + # Remove block_ports
202 + while IFS= read -r entry; do
203 + [[ -z "$entry" ]] && continue
204 + local target port proto
205 + IFS=":" read -r target port proto <<< "$entry"
206 + proto="${proto:-tcp}"
207 + fw::unblock_port "$client_ip" "$target" "$port" "$proto"
208 + done < <(rule::get "$rule_name" "block_ports")
209 +
210 + # Remove block_ips
211 + while IFS= read -r block_ip; do
212 + [[ -z "$block_ip" ]] && continue
213 + if [[ "$block_ip" == *"/"* ]]; then
214 + fw::unblock_subnet "$client_ip" "$block_ip"
215 + else
216 + fw::unblock_ip "$client_ip" "$block_ip"
217 + fi
218 + done < <(rule::get "$rule_name" "block_ips")
219 +
220 + # Remove DNS redirect if applicable
221 + local dns_redirect
222 + dns_redirect=$(rule::get "$rule_name" "dns_redirect")
223 + if [[ "$dns_redirect" == "true" ]]; then
224 + local subtype
225 + subtype=$(peers::get_meta "$peer_name" "subtype")
226 + local subnet
227 + if [[ -n "$subtype" ]]; then
228 + subnet=$(config::subnet_for "$subtype")
229 + else
230 + local peer_type
231 + peer_type=$(peers::get_type "$peer_name") || true
232 + [[ -z "$peer_type" ]] && peer_type="phone"
233 + subnet=$(config::subnet_for "$peer_type")
234 + fi
235 + rule::remove_dns_redirect "${subnet}.0/24"
236 + fi
237 +
238 + # Clear rule from meta
239 + [[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" ""
240 +
241 + log::debug "Removed rule '${rule_name}' from: ${client_ip}"
242 + }
243 +
244 + function rule::reapply_all() {
245 + local rule_name="${1:-}"
246 + rule::require_exists "$rule_name" || return 1
247 +
248 + local peers=()
249 + mapfile -t peers < <(peers::with_rule "$rule_name")
250 + [[ ${#peers[@]} -eq 0 ]] && return 0
251 +
252 + local count=0
253 + for peer_name in "${peers[@]}"; do
254 + local client_ip
255 + client_ip=$(peers::get_ip "$peer_name")
256 + [[ -z "$client_ip" ]] && continue
257 + # FLUSH first to ensure clean ordering
258 + fw::flush_peer "$client_ip"
259 + rule::apply "$rule_name" "$client_ip" "$peer_name"
260 + (( count++ )) || true
261 + done
262 +
263 + log::wg_success "Rule '${rule_name}' re-applied to ${count} peers"
264 + }
265 +
266 + function rule::restore_all() {
267 + while IFS= read -r peer_name; do
268 + # Skip blocked peers - no fw rules needed when blocked
269 + block::is_blocked "$peer_name" && continue
270 +
271 + local rule_name
272 + rule_name=$(peers::get_meta "$peer_name" "rule")
273 +
274 + [[ -z "$rule_name" ]] && continue
275 +
276 + if ! rule::exists "$rule_name"; then
277 + log::wg_warning "Rule '${rule_name}' not found for peer '${peer_name}', skipping"
278 + continue
279 + fi
280 +
281 + local client_ip
282 + client_ip=$(peers::get_ip "$peer_name")
283 + [[ -z "$client_ip" ]] && continue
284 +
285 + rule::apply "$rule_name" "$client_ip"
286 + done < <(peers::all)
287 + log::wg "Rules restored for all peers"
288 + }
289 +
290 + # ============================================
291 + # Rendering
292 + # ============================================
293 +
294 + function rule::render_flat() {
295 + local rule_name="${1:-}"
296 +
297 + local allow_ports allow_ips block_ips block_ports dns
298 + allow_ports=$(rule::get "$rule_name" "allow_ports")
299 + allow_ips=$(rule::get "$rule_name" "allow_ips")
300 + block_ips=$(rule::get "$rule_name" "block_ips")
301 + block_ports=$(rule::get "$rule_name" "block_ports")
302 + dns=$(rule::get_own "$rule_name" "dns_redirect")
303 +
304 + local has_content=false
305 + [[ -n "${allow_ports}${allow_ips}${block_ips}${block_ports}" ]] && \
306 + has_content=true
307 +
308 + if ! $has_content; then
309 + printf "\n full access (no restrictions)\n"
310 + return 0
311 + fi
312 +
313 + if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
314 + printf "\n"
315 + while IFS= read -r e; do
316 + [[ -z "$e" ]] && continue
317 + net::print_entry "+" "$e" 2
318 + done <<< "$allow_ports"$'\n'"$allow_ips"
319 + fi
320 +
321 + if [[ -n "$block_ips" || -n "$block_ports" ]]; then
322 + printf "\n"
323 + while IFS= read -r e; do
324 + [[ -z "$e" ]] && continue
325 + net::print_entry "-" "$e" 2
326 + done <<< "$block_ips"$'\n'"$block_ports"
327 + fi
328 +
329 + [[ "${dns,,}" == "true" ]] && \
330 + net::print_dns_redirect "$(config::dns)" 6 "DNS"
331 +
332 + return 0
333 + }
334 +
335 + function rule::render_entries() {
336 + # Renders allow/block entries for a rule name with annotations and DNS
337 + # Usage: rule::render_entries <rule_name> <indent>
338 + # indent: 4 for rule show, 4 for inspect (same)
339 + local rule_name="${1:-}" indent="${2:-4}"
340 + local rule_file
341 + rule_file="$(rule::path "$rule_name")"
342 +
343 + local allow_ports allow_ips block_ips block_ports dns
344 + allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true)
345 + allow_ips=$(rule::get "$rule_name" "allow_ips" 2>/dev/null || true)
346 + block_ips=$(rule::get "$rule_name" "block_ips" 2>/dev/null || true)
347 + block_ports=$(rule::get "$rule_name" "block_ports" 2>/dev/null || true)
348 + dns=$(rule::get_own "$rule_name" "dns_redirect")
349 +
350 + while IFS= read -r e; do
351 + [[ -z "$e" ]] && continue
352 + net::print_entry "+" "$e"
353 + done <<< "$allow_ports"$'\n'"$allow_ips"
354 +
355 + while IFS= read -r e; do
356 + [[ -z "$e" ]] && continue
357 + net::print_entry "-" "$e"
358 + done <<< "$block_ips"$'\n'"$block_ports"
359 +
360 + [[ "${dns,,}" == "true" ]] && \
361 + net::print_dns_redirect "$(config::dns)" 6 "DNS"
362 + }
363 +
364 + function rule::render_own_entries() {
365 + # Renders own (non-inherited) entries for a rule
366 + local rule_name="${1:-}"
367 + local rule_file
368 + rule_file="$(rule::path "$rule_name")"
369 +
370 + local allow_ports allow_ips block_ips block_ports dns
371 + allow_ports=$(json::get "$rule_file" "allow_ports" 2>/dev/null || true)
372 + allow_ips=$(json::get "$rule_file" "allow_ips" 2>/dev/null || true)
373 + block_ips=$(json::get "$rule_file" "block_ips" 2>/dev/null || true)
374 + block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true)
375 + dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true)
376 +
377 + local has_own=false
378 + local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}"
379 + [[ -n "${combined//[$'\n']/}" ]] && has_own=true
380 +
381 + $has_own || return 0
382 +
383 + while IFS= read -r e; do
384 + [[ -z "$e" ]] && continue
385 + net::print_entry "+" "$e"
386 + done <<< "$allow_ports"$'\n'"$allow_ips"
387 +
388 + while IFS= read -r e; do
389 + [[ -z "$e" ]] && continue
390 + net::print_entry "-" "$e"
391 + done <<< "$block_ips"$'\n'"$block_ports"
392 +
393 + [[ "${dns,,}" == "true" ]] && \
394 + net::print_dns_redirect "$(config::dns)" 6 "DNS"
395 +
396 + return 0
397 + }
398 +
399 + function rule::render_extends_tree() {
400 + # Renders full inheritance tree for a rule
401 + local rule_name="${1:-}"
402 + local rule_file
403 + rule_file="$(rule::path "$rule_name")"
404 +
405 + local extends_raw=()
406 + mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true)
407 +
408 + [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]:-}" ]] && return 1
409 +
410 + for base_name in "${extends_raw[@]}"; do
411 + [[ -z "$base_name" ]] && continue
412 + printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name"
413 + rule::render_entries "$base_name"
414 + done
415 +
416 + # Own rules after inherited
417 + local own_output
418 + own_output=$(rule::render_own_entries "$rule_name")
419 + if [[ -n "$own_output" ]]; then
420 + printf "\n \033[0;37mOwn:\033[0m\n"
421 + printf "%s\n" "$own_output"
422 + fi
423 +
424 + return 0
425 + }
426 +
427 + # ============================================
428 + # DNS Redirect
429 + # ============================================
430 +
431 + function rule::apply_dns_redirect() {
432 + local client_subnet="${1:-}"
433 + fw::nat_add_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
434 + }
435 +
436 + function rule::remove_dns_redirect() {
437 + local client_subnet="${1:-}"
438 + fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
439 + }
Newer Older