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

nuno ревизий этого фрагмента 1 month ago. К ревизии

1 file changed, 1594 insertions

gistfile1.txt(файл создан)

@@ -0,0 +1,1594 @@
1 + #!/usr/bin/env python3
2 + """
3 + wgctl JSON helper — called by shell functions to read/write JSON files.
4 + Usage: json_helper.py <command> <file> [key] [value]
5 + """
6 +
7 + import sys
8 + import json
9 + import os
10 +
11 + DATETIME_FMT = os.environ.get('WGCTL_DATETIME_FMT', '%Y-%m-%d %H:%M')
12 +
13 + def get(file, key):
14 + try:
15 + with open(file) as f:
16 + data = json.load(f)
17 + val = data.get(key, [])
18 + if isinstance(val, bool):
19 + print(str(val).lower()) # true/false not True/False
20 + elif isinstance(val, list):
21 + if val:
22 + print('\n'.join(str(v) for v in val))
23 + else:
24 + if val:
25 + print(val)
26 + except:
27 + sys.exit(0)
28 +
29 + def set_key(file, key, value):
30 + try:
31 + data = {}
32 + if os.path.exists(file):
33 + with open(file) as f:
34 + data = json.load(f)
35 + # Try to parse as JSON value first (for arrays/bools)
36 + try:
37 + data[key] = json.loads(value)
38 + except:
39 + data[key] = value
40 + with open(file, 'w') as f:
41 + json.dump(data, f, indent=2)
42 + except Exception as e:
43 + print(f"Error: {e}", file=sys.stderr)
44 + sys.exit(1)
45 +
46 + def delete_key(file, key):
47 + try:
48 + with open(file) as f:
49 + data = json.load(f)
50 + data.pop(key, None)
51 + with open(file, 'w') as f:
52 + json.dump(data, f, indent=2)
53 + except Exception as e:
54 + print(f"Error: {e}", file=sys.stderr)
55 + sys.exit(1)
56 +
57 + def append(file, key, value):
58 + try:
59 + data = {}
60 + if os.path.exists(file):
61 + with open(file) as f:
62 + data = json.load(f)
63 + if key not in data:
64 + data[key] = []
65 + if value not in data[key]:
66 + data[key].append(value)
67 + with open(file, 'w') as f:
68 + json.dump(data, f, indent=2)
69 + except Exception as e:
70 + print(f"Error: {e}", file=sys.stderr)
71 + sys.exit(1)
72 +
73 + def remove_value(file, key, value):
74 + try:
75 + with open(file) as f:
76 + data = json.load(f)
77 + if key in data and value in data[key]:
78 + data[key].remove(value)
79 + with open(file, 'w') as f:
80 + json.dump(data, f, indent=2)
81 + except Exception as e:
82 + print(f"Error: {e}", file=sys.stderr)
83 + sys.exit(1)
84 +
85 + def cat(file):
86 + try:
87 + with open(file) as f:
88 + data = json.load(f)
89 + print(json.dumps(data, indent=2))
90 + except Exception as e:
91 + sys.exit(1)
92 +
93 + def has_key(file, key):
94 + try:
95 + with open(file) as f:
96 + data = json.load(f)
97 + sys.exit(0 if key in data else 1)
98 + except:
99 + sys.exit(1)
100 +
101 + def filter_values(file, key, value):
102 + """Remove all entries where value matches"""
103 + try:
104 + with open(file) as f:
105 + data = json.load(f)
106 + data = {k: v for k, v in data.items() if v != value}
107 + with open(file, 'w') as f:
108 + json.dump(data, f, indent=2)
109 + except Exception as e:
110 + print(f"Error: {e}", file=sys.stderr)
111 + sys.exit(1)
112 +
113 + def last_event(file, key, field, client):
114 + """Get last event field for a client"""
115 + try:
116 + last = None
117 + with open(file) as f:
118 + for line in f:
119 + try:
120 + e = json.loads(line.strip())
121 + if e.get(key) == client:
122 + last = e
123 + except:
124 + pass
125 + if last:
126 + print(last.get(field, ''))
127 + except:
128 + pass
129 +
130 + def events_for(file, ip, limit):
131 + """Format events for a given IP"""
132 + try:
133 + from datetime import datetime
134 + events = []
135 + with open(file) as f:
136 + for line in f:
137 + try:
138 + e = json.loads(line.strip())
139 + if e.get('ip') == ip:
140 + events.append(e)
141 + except:
142 + pass
143 + for e in events[-int(limit):]:
144 + ts = e.get('timestamp', '')
145 + try:
146 + dt = datetime.fromisoformat(ts)
147 + ts = dt.strftime(DATETIME_FMT)
148 + except:
149 + pass
150 + endpoint = e.get('endpoint', '—')
151 + client = e.get('client', '—')
152 + event = e.get('event', '—')
153 + print(f' {ts} {client:<20} {endpoint:<20} {event}')
154 + except:
155 + pass
156 +
157 + def fw_events(file, filter_ip, filter_type, clients_dir, limit):
158 + """Format firewall drop events with dedup and counts"""
159 + import glob
160 + proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
161 +
162 + ip_to_name = {}
163 + for conf in glob.glob(f"{clients_dir}/*.conf"):
164 + name = os.path.basename(conf).replace('.conf', '')
165 + try:
166 + with open(conf) as f:
167 + for line in f:
168 + if line.startswith('Address'):
169 + ip = line.split('=')[1].strip().split('/')[0]
170 + ip_to_name[ip] = name
171 + except:
172 + pass
173 +
174 + events = []
175 + last_seen = {}
176 +
177 + try:
178 + with open(file) as f:
179 + for line in f:
180 + try:
181 + e = json.loads(line.strip())
182 + src = e.get('src_ip', '')
183 + if not src:
184 + continue
185 + if filter_ip and src != filter_ip:
186 + continue
187 + dst = e.get('dest_ip', '')
188 + port = e.get('dest_port', '')
189 + proto = e.get('ip.protocol', 0)
190 + key = (src, dst, port, proto)
191 + ts_str = e.get('timestamp', '')
192 + try:
193 + from datetime import datetime
194 + ts = datetime.fromisoformat(ts_str).timestamp()
195 + except:
196 + ts = 0
197 + windows = {1: 5, 6: 30, 17: 10}
198 + window = windows.get(proto, 10)
199 + if key in last_seen and (ts - last_seen[key]) < window:
200 + continue
201 + last_seen[key] = ts
202 + events.append(e)
203 + except:
204 + pass
205 + except:
206 + pass
207 +
208 + # Dedup consecutive same src+dst+port within 60s with count
209 + deduped = []
210 + counts = []
211 + for e in events:
212 + ts_str = e.get('timestamp', '')
213 + try:
214 + from datetime import datetime
215 + ts = datetime.fromisoformat(ts_str).timestamp()
216 + except:
217 + ts = 0
218 + src = e.get('src_ip', '')
219 + dst = e.get('dest_ip', '')
220 + port = e.get('dest_port', '')
221 + proto = e.get('ip.protocol', 0)
222 + key = (src, dst, port, proto)
223 +
224 + if deduped and counts:
225 + prev = deduped[-1]
226 + try:
227 + prev_ts = datetime.fromisoformat(prev.get('timestamp','')).timestamp()
228 + except:
229 + prev_ts = 0
230 + prev_key = (prev.get('src_ip',''), prev.get('dest_ip',''),
231 + prev.get('dest_port',''), prev.get('ip.protocol',0))
232 + if key == prev_key and (ts - prev_ts) < 60:
233 + counts[-1] += 1
234 + continue
235 +
236 + deduped.append(e)
237 + counts.append(1)
238 +
239 + grouped = []
240 + group_counts = []
241 + for e in deduped:
242 + ts_str = e.get('timestamp', '')
243 + try:
244 + from datetime import datetime
245 + dt = datetime.fromisoformat(ts_str)
246 + # Truncate to minute for grouping
247 + minute_key = dt.strftime('%Y-%m-%d %H:%M')
248 + except:
249 + minute_key = ts_str[:16]
250 +
251 + src = e.get('src_ip', '')
252 + dst = e.get('dest_ip', '')
253 + port = e.get('dest_port', '')
254 + proto = e.get('ip.protocol', 0)
255 + key = (minute_key, src, dst, proto) # group within same minute
256 +
257 + if grouped and group_counts:
258 + prev = grouped[-1]
259 + try:
260 + prev_dt = datetime.fromisoformat(prev.get('timestamp',''))
261 + prev_minute = prev_dt.strftime('%Y-%m-%d %H:%M')
262 + except:
263 + prev_minute = ''
264 + prev_key = (prev_minute, prev.get('src_ip',''),
265 + prev.get('dest_ip',''), prev.get('ip.protocol',0))
266 + if key == prev_key:
267 + group_counts[-1] += 1
268 + continue
269 +
270 + grouped.append(e)
271 + group_counts.append(1)
272 +
273 + for e, count in zip(deduped[-int(limit):], counts[-int(limit):]):
274 + ts = e.get('timestamp', '')
275 + try:
276 + from datetime import datetime
277 + dt = datetime.fromisoformat(ts)
278 + ts = dt.strftime(DATETIME_FMT)
279 + except:
280 + pass
281 + src = e.get('src_ip', '—')
282 + dst = e.get('dest_ip', '—')
283 + port = e.get('dest_port', '')
284 + proto_num = e.get('ip.protocol', 0)
285 + proto = proto_map.get(proto_num, str(proto_num))
286 + dst_str = f"{dst}:{port}" if port else dst
287 + client = ip_to_name.get(src, src)
288 + if filter_type and not client.startswith(filter_type + '-'):
289 + continue
290 + count_str = f" (x{count})" if count > 1 else ""
291 + print(f"{ts}|{client}|{dst_str}|{proto}{count_str}")
292 +
293 + def wg_events(file, filter_client, filter_type, limit):
294 + """Format WireGuard events from events.log with dedup"""
295 + events = []
296 + try:
297 + with open(file) as f:
298 + for line in f:
299 + try:
300 + e = json.loads(line.strip())
301 + client = e.get('client', '')
302 + if not client:
303 + continue
304 + if filter_client and client != filter_client:
305 + continue
306 + if filter_type and not client.startswith(filter_type + '-'):
307 + continue
308 + events.append(e)
309 + except:
310 + pass
311 + except:
312 + pass
313 +
314 + # Dedup consecutive same client+event+endpoint within 60s
315 + deduped = []
316 + counts = []
317 + for e in events:
318 + ts_str = e.get('timestamp', '')
319 + try:
320 + from datetime import datetime
321 + ts = datetime.fromisoformat(ts_str).timestamp()
322 + except:
323 + ts = 0
324 + client = e.get('client', '')
325 + event = e.get('event', '')
326 + endpoint = e.get('endpoint', '')
327 + key = (client, event, endpoint[:15])
328 +
329 + if deduped and counts:
330 + prev = deduped[-1]
331 + prev_ts_str = prev.get('timestamp', '')
332 + try:
333 + prev_ts = datetime.fromisoformat(prev_ts_str).timestamp()
334 + except:
335 + prev_ts = 0
336 + prev_key = (prev.get('client',''), prev.get('event',''), prev.get('endpoint','')[:15])
337 + if key == prev_key and (ts - prev_ts) < 60:
338 + counts[-1] += 1
339 + continue
340 +
341 + deduped.append(e)
342 + counts.append(1)
343 +
344 + for e, count in zip(deduped[-int(limit):], counts[-int(limit):]):
345 + ts = e.get('timestamp', '')
346 + try:
347 + from datetime import datetime
348 + dt = datetime.fromisoformat(ts)
349 + ts = dt.strftime(DATETIME_FMT)
350 + except:
351 + pass
352 + client = e.get('client', '—')
353 + endpoint = e.get('endpoint', '—')
354 + event = e.get('event', '—')
355 + count_str = f" (x{count})" if count > 1 else ""
356 + print(f"{ts}|{client}|{endpoint}|{event}{count_str}")
357 +
358 + def format_fw_event(line, clients_dir):
359 + """Format a single fw_event line"""
360 + import glob
361 + proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
362 +
363 + # Build ip->name map
364 + ip_to_name = {}
365 + for conf in glob.glob(f"{clients_dir}/*.conf"):
366 + name = os.path.basename(conf).replace('.conf', '')
367 + try:
368 + with open(conf) as f:
369 + for l in f:
370 + if l.startswith('Address'):
371 + ip = l.split('=')[1].strip().split('/')[0]
372 + ip_to_name[ip] = name
373 + except:
374 + pass
375 +
376 + try:
377 + e = json.loads(line.strip())
378 + src = e.get('src_ip', '')
379 + if not src:
380 + return None
381 + ts = e.get('timestamp', '')
382 + try:
383 + from datetime import datetime
384 + dt = datetime.fromisoformat(ts)
385 + ts = dt.strftime(DATETIME_FMT)
386 + except:
387 + pass
388 + dst = e.get('dest_ip', '—')
389 + port = e.get('dest_port', '')
390 + proto_num = e.get('ip.protocol', 0)
391 + proto = proto_map.get(proto_num, str(proto_num))
392 + dst_str = f"{dst}:{port}" if port else dst
393 + client = ip_to_name.get(src, src)
394 + return f"{ts}|{client}|{dst_str}|{proto}"
395 + except:
396 + return None
397 +
398 + def format_wg_event(line):
399 + """Format a single wg_event line"""
400 + try:
401 + e = json.loads(line.strip())
402 + client = e.get('client', '')
403 + if not client:
404 + return None
405 + ts = e.get('timestamp', '')
406 + try:
407 + from datetime import datetime
408 + dt = datetime.fromisoformat(ts)
409 + ts = dt.strftime(DATETIME_FMT)
410 + except:
411 + pass
412 + endpoint = e.get('endpoint', '—')
413 + event = e.get('event', '—')
414 + return f"{ts}|{client}|{endpoint}|{event}|wg"
415 + except:
416 + return None
417 +
418 + def remove_events(file, identifier):
419 + """Remove all events for a client/ip from a JSONL file"""
420 + try:
421 + lines = []
422 + with open(file) as f:
423 + for line in f:
424 + try:
425 + e = json.loads(line.strip())
426 + if e.get('client') == identifier or e.get('src_ip') == identifier:
427 + continue
428 + lines.append(line)
429 + except:
430 + lines.append(line)
431 + with open(file, 'w') as f:
432 + f.writelines(lines)
433 + except Exception as e:
434 + print(f"Error: {e}", file=sys.stderr)
435 + sys.exit(1)
436 +
437 + def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_peers=""):
438 + """Follow both log files and output formatted events"""
439 + import glob, time, select
440 +
441 + proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
442 + peer_filter = set(filter_peers.split(',')) if filter_peers else set()
443 +
444 + # Build ip->name map
445 + ip_to_name = {}
446 + for conf in glob.glob(f"{clients_dir}/*.conf"):
447 + name = os.path.basename(conf).replace('.conf', '')
448 + try:
449 + with open(conf) as f:
450 + for l in f:
451 + if l.startswith('Address'):
452 + ip = l.split('=')[1].strip().split('/')[0]
453 + ip_to_name[ip] = name
454 + except:
455 + pass
456 +
457 + # Open files and seek to end
458 + files = {}
459 + for label, path in [('fw', fw_file), ('wg', wg_file)]:
460 + if path and os.path.exists(path):
461 + f = open(path)
462 + f.seek(0, 2) # seek to end
463 + files[label] = f
464 +
465 + dedup = {}
466 +
467 + try:
468 + while True:
469 + for label, f in files.items():
470 + line = f.readline()
471 + if not line:
472 + continue
473 + try:
474 + e = json.loads(line.strip())
475 + except:
476 + continue
477 +
478 + if label == 'fw':
479 + src = e.get('src_ip', '')
480 + if not src:
481 + continue
482 + if filter_ip and src != filter_ip:
483 + continue
484 +
485 + # Filter by peer names if specified
486 + if peer_filter:
487 + client_name = ip_to_name.get(src, '')
488 + if client_name not in peer_filter:
489 + continue
490 + dst = e.get('dest_ip', '—')
491 + port = e.get('dest_port', '')
492 + proto_num = e.get('ip.protocol', 0)
493 + proto = proto_map.get(proto_num, str(proto_num))
494 +
495 + # Dedup
496 + key = (src, dst, port, proto_num)
497 + windows = {1: 5, 6: 30, 17: 10}
498 + window = windows.get(proto_num, 10)
499 + now = time.time()
500 + if key in dedup and (now - dedup[key]) < window:
501 + continue
502 + dedup[key] = now
503 +
504 + client = ip_to_name.get(src, src)
505 + if filter_type and not client.startswith(filter_type + '-'):
506 + continue
507 + dst_str = f"{dst}:{port}" if port else dst
508 + ts = e.get('timestamp', '')[:16].replace('T', ' ')
509 + print(f"fw|{ts}|{client}|{dst_str}|{proto}", flush=True)
510 +
511 + elif label == 'wg':
512 + client = e.get('client', '')
513 + if not client:
514 + continue
515 + if filter_ip:
516 + ip = ip_to_name.get(filter_ip, '')
517 + if client != ip and client != filter_ip:
518 + continue
519 +
520 + if peer_filter and client not in peer_filter:
521 + continue
522 + if filter_type and not client.startswith(filter_type + '-'):
523 + continue
524 + ts = e.get('timestamp', '')[:16].replace('T', ' ')
525 + endpoint = e.get('endpoint', '—')
526 + event = e.get('event', '—')
527 + print(f"wg|{ts}|{client}|{endpoint}|{event}", flush=True)
528 +
529 + time.sleep(0.1)
530 + except KeyboardInterrupt:
531 + pass
532 +
533 + def count(file, key):
534 + try:
535 + with open(file) as f:
536 + data = json.load(f)
537 + val = data.get(key, [])
538 + print(len(val) if isinstance(val, list) else 0)
539 + except:
540 + print(0)
541 +
542 + def audit_fw_counts(clients_dir):
543 + """Return peer_name:fw_count pairs from iptables and client configs"""
544 + import glob, subprocess
545 +
546 + # Get iptables output once
547 + try:
548 + result = subprocess.run(
549 + ['iptables', '-L', 'FORWARD', '-n'],
550 + capture_output=True, text=True
551 + )
552 + fw_output = result.stdout
553 + except:
554 + fw_output = ""
555 +
556 + # Build ip->name and count rules
557 + for conf in glob.glob(f"{clients_dir}/*.conf"):
558 + name = os.path.basename(conf).replace('.conf', '')
559 + try:
560 + with open(conf) as f:
561 + for line in f:
562 + if line.startswith('Address'):
563 + ip = line.split('=')[1].strip().split('/')[0]
564 + count = fw_output.count(ip)
565 + print(f"{name}:{count}")
566 + break
567 + except:
568 + pass
569 +
570 + def peer_group_map(groups_dir):
571 + """Return peer:group pairs for all groups"""
572 + import glob
573 + try:
574 + for group_file in glob.glob(f"{groups_dir}/*.group"):
575 + try:
576 + with open(group_file) as f:
577 + g = json.load(f)
578 + name = g.get('name', '')
579 + for peer in g.get('peers', []):
580 + if peer:
581 + print(f"{peer}:{name}")
582 + except:
583 + pass
584 + except:
585 + pass
586 +
587 + def peer_groups(groups_dir, peer_name):
588 + """Find all groups containing a peer"""
589 + import glob
590 + try:
591 + for group_file in glob.glob(f"{groups_dir}/*.group"):
592 + try:
593 + with open(group_file) as f:
594 + g = json.load(f)
595 + if peer_name in g.get('peers', []):
596 + print(g.get('name', ''))
597 + except:
598 + pass
599 + except:
600 + pass
601 +
602 + def peer_data(clients_dir, meta_dir, events_log):
603 + import glob
604 +
605 + meta = {}
606 + for f in glob.glob(f"{meta_dir}/*.meta"):
607 + name = os.path.basename(f).replace('.meta', '')
608 + try:
609 + with open(f) as mf:
610 + meta[name] = json.load(mf)
611 + except:
612 + meta[name] = {}
613 +
614 + last_events = {}
615 + try:
616 + with open(events_log) as f:
617 + for line in f:
618 + try:
619 + e = json.loads(line.strip())
620 + client = e.get('client', '')
621 + if client:
622 + last_events[client] = e
623 + except:
624 + pass
625 + except:
626 + pass
627 +
628 + for conf in sorted(glob.glob(f"{clients_dir}/*.conf")):
629 + name = os.path.basename(conf).replace('.conf', '')
630 + ip = ''
631 + try:
632 + with open(conf) as f:
633 + for line in f:
634 + if line.startswith('Address'):
635 + ip = line.split('=')[1].strip().split('/')[0]
636 + break
637 + except:
638 + pass
639 +
640 + m = meta.get(name, {})
641 + rule = m.get('rule', '')
642 + subtype = m.get('subtype', '')
643 + main_group = m.get('main_group', '')
644 +
645 + last_event = last_events.get(name, {})
646 + last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
647 + last_evt = last_event.get('event', '') # fixed: was last_event
648 +
649 + print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}|{main_group}")
650 +
651 + def iso_to_ts(iso_str):
652 + """Convert ISO timestamp to unix timestamp"""
653 + try:
654 + from datetime import datetime, timezone
655 + dt = datetime.fromisoformat(iso_str)
656 + if dt.tzinfo is None:
657 + dt = dt.replace(tzinfo=timezone.utc)
658 + print(int(dt.timestamp()))
659 + except:
660 + print(0)
661 +
662 + def rule_list_data(rules_dir, meta_dir):
663 + """Return all rule data including base rules and extends"""
664 + import glob
665 +
666 + rule_peer_counts = {}
667 + for f in glob.glob(f"{meta_dir}/*.meta"):
668 + try:
669 + with open(f) as mf:
670 + meta = json.load(mf)
671 + rule = meta.get('rule', '')
672 + if rule:
673 + rule_peer_counts[rule] = rule_peer_counts.get(rule, 0) + 1
674 + except:
675 + pass
676 +
677 + rule_files = (
678 + sorted(glob.glob(f"{rules_dir}/*.rule")) +
679 + sorted(glob.glob(f"{rules_dir}/base/*.rule"))
680 + )
681 +
682 + # Collect all data first
683 + rules_data = []
684 + for rule_file in rule_files:
685 + is_base = '/base/' in rule_file
686 + try:
687 + with open(rule_file) as f:
688 + r = json.load(f)
689 + name = r.get('name', '')
690 + desc = r.get('desc', '')
691 + group = r.get('group', '')
692 + extends = ','.join(r.get('extends', []))
693 + resolved = _rule_resolve_internal(rules_dir, name)
694 + n_allows = len(resolved.get('allow_ips', [])) + \
695 + len(resolved.get('allow_ports', []))
696 + n_blocks = len(resolved.get('block_ips', [])) + \
697 + len(resolved.get('block_ports', []))
698 + peer_count = rule_peer_counts.get(name, 0)
699 + rules_data.append({
700 + 'name': name, 'desc': desc, 'n_allows': n_allows,
701 + 'n_blocks': n_blocks, 'peer_count': peer_count,
702 + 'extends': extends, 'is_base': is_base, 'group': group
703 + })
704 + except:
705 + pass
706 +
707 + # Sort: non-base first, then by group (empty group last within non-base),
708 + # then by name within group
709 + rules_data.sort(key=lambda x: (
710 + x['is_base'],
711 + x['group'] == '' and not x['is_base'],
712 + x['group'],
713 + x['name']
714 + ))
715 +
716 + for r in rules_data:
717 + print(f"{r['name']}|{r['desc']}|{r['n_allows']}|{r['n_blocks']}|"
718 + f"{r['peer_count']}|{r['extends']}|{r['is_base']}|{r['group']}")
719 +
720 + def group_list_data(groups_dir, blocks_dir):
721 + """Return group summary data in one call"""
722 + import glob
723 +
724 + # Get all block files
725 + blocked_peers = set()
726 + for f in glob.glob(f"{blocks_dir}/*.block"):
727 + name = os.path.basename(f).replace('.block', '')
728 + blocked_peers.add(name)
729 +
730 + for group_file in sorted(glob.glob(f"{groups_dir}/*.group")):
731 + try:
732 + with open(group_file) as f:
733 + g = json.load(f)
734 + name = g.get('name', '')
735 + desc = g.get('desc', '')
736 + peers = [p for p in g.get('peers', []) if p]
737 + total = len(peers)
738 + blocked = sum(1 for p in peers if p in blocked_peers)
739 + print(f"{name}|{desc}|{total}|{blocked}")
740 + except:
741 + pass
742 +
743 + def fmt_datetime(iso_str, fmt):
744 + """Format ISO timestamp with given strftime format"""
745 + try:
746 + from datetime import datetime
747 + dt = datetime.fromisoformat(iso_str)
748 + print(dt.strftime(fmt))
749 + except:
750 + print(iso_str)
751 +
752 + def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips,
753 + block_ports, allow_ports='', extends='', group=''):
754 + rule = {
755 + 'name': name,
756 + 'desc': desc,
757 + 'group': group,
758 + 'dns_redirect': dns_redirect == 'true',
759 + 'extends': [x for x in extends.split(',') if x] if extends else [],
760 + 'allow_ips': [x for x in allow_ips.split(',') if x] if allow_ips else [],
761 + 'allow_ports': [x for x in allow_ports.split(',') if x] if allow_ports else [],
762 + 'block_ips': [x for x in block_ips.split(',') if x] if block_ips else [],
763 + 'block_ports': [x for x in block_ports.split(',') if x] if block_ports else [],
764 + }
765 + with open(file, 'w') as f:
766 + json.dump(rule, f, indent=2)
767 +
768 + def cleanup_config(config_file):
769 + """Normalize blank lines in WireGuard config"""
770 + import re
771 + try:
772 + with open(config_file) as f:
773 + config = f.read()
774 + config = re.sub(r'\n{3,}', '\n\n', config)
775 + config = config.rstrip('\n') + '\n'
776 + with open(config_file, 'w') as f:
777 + f.write(config)
778 + except Exception as e:
779 + print(f"Error: {e}", file=sys.stderr)
780 + sys.exit(1)
781 +
782 + def remove_peer_block(config_file, name):
783 + """Remove a peer block from WireGuard config by name"""
784 + import re
785 + try:
786 + with open(config_file) as f:
787 + config = f.read()
788 + pattern = r'\n\[Peer\]\n# ' + re.escape(name) + r'\n[^\n]+\n[^\n]+\n'
789 + result = re.sub(pattern, '\n', config)
790 + with open(config_file, 'w') as f:
791 + f.write(result)
792 + except Exception as e:
793 + print(f"Error: {e}", file=sys.stderr)
794 + sys.exit(1)
795 +
796 + def create_group(file, name, desc):
797 + """Create a new group JSON file"""
798 + try:
799 + group = {'name': name, 'desc': desc, 'peers': []}
800 + with open(file, 'w') as f:
801 + json.dump(group, f, indent=2)
802 + except Exception as e:
803 + print(f"Error: {e}", file=sys.stderr)
804 + sys.exit(1)
805 +
806 + def parse_event(line):
807 + """Parse a single JSON event line"""
808 + try:
809 + e = json.loads(line)
810 + print(f"{e.get('timestamp','')}|{e.get('client','')}|{e.get('endpoint','')}|{e.get('event','')}")
811 + except:
812 + pass
813 +
814 + def parse_fw_event(line):
815 + """Parse a single fw_events.log JSON line"""
816 + try:
817 + e = json.loads(line)
818 + ts = e.get('timestamp', '')
819 + src = e.get('src_ip', '')
820 + dst = e.get('dest_ip', '')
821 + port = e.get('dest_port', '')
822 + proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
823 + proto_num = e.get('ip.protocol', 0)
824 + proto = proto_map.get(proto_num, str(proto_num))
825 + print(f"{ts}|{src}|{dst}|{port}|{proto}")
826 + except:
827 + pass
828 +
829 + def peer_transfer(wg_interface):
830 + """Get total transfer bytes per peer"""
831 + import subprocess
832 + low = int(os.environ.get('ACTIVITY_TOTAL_LOW_BYTES', '1000000'))
833 + med = int(os.environ.get('ACTIVITY_TOTAL_MED_BYTES', '10000000'))
834 + high = int(os.environ.get('ACTIVITY_TOTAL_HIGH_BYTES', '100000000'))
835 + try:
836 + result = subprocess.run(
837 + ['wg', 'show', wg_interface, 'transfer'],
838 + capture_output=True, text=True
839 + )
840 + for line in result.stdout.strip().split('\n'):
841 + if not line:
842 + continue
843 + parts = line.split('\t')
844 + if len(parts) == 3:
845 + pubkey, rx, tx = parts
846 + total = int(rx) + int(tx)
847 + if total == 0: level = 'none'
848 + elif total < low: level = 'low'
849 + elif total < med: level = 'medium'
850 + elif total < high: level = 'high'
851 + else: level = 'very high'
852 + print(f"{pubkey}|{rx}|{tx}|{level}")
853 + except:
854 + pass
855 +
856 + def peer_transfer_delta(wg_interface, cache_file):
857 + """Calculate current transfer rate using delta from previous sample"""
858 + import subprocess, time
859 +
860 + low = int(os.environ.get('ACTIVITY_CURRENT_LOW_BYTES', '10000')) # 10KB/s
861 + med = int(os.environ.get('ACTIVITY_CURRENT_MED_BYTES', '100000')) # 100KB/s
862 + high = int(os.environ.get('ACTIVITY_CURRENT_HIGH_BYTES', '1000000')) # 1MB/s
863 +
864 + current = {}
865 + now = time.time()
866 + try:
867 + result = subprocess.run(
868 + ['wg', 'show', wg_interface, 'transfer'],
869 + capture_output=True, text=True
870 + )
871 + for line in result.stdout.strip().split('\n'):
872 + if not line:
873 + continue
874 + parts = line.split('\t')
875 + if len(parts) == 3:
876 + pubkey, rx, tx = parts
877 + current[pubkey] = {'rx': int(rx), 'tx': int(tx), 'ts': now}
878 + except:
879 + pass
880 +
881 + prev = {}
882 + if os.path.exists(cache_file):
883 + try:
884 + with open(cache_file) as f:
885 + prev = json.load(f)
886 + except:
887 + pass
888 +
889 + try:
890 + with open(cache_file, 'w') as f:
891 + json.dump(current, f)
892 + except:
893 + pass
894 +
895 + for pubkey, data in current.items():
896 + if pubkey in prev:
897 + dt = data['ts'] - prev[pubkey].get('ts', data['ts'])
898 + if dt > 0:
899 + rx_rate = max(0, (data['rx'] - prev[pubkey]['rx']) / dt)
900 + tx_rate = max(0, (data['tx'] - prev[pubkey]['tx']) / dt)
901 + total = rx_rate + tx_rate
902 + if total <= 0: level = 'idle'
903 + elif total < low: level = 'low'
904 + elif total < med: level = 'medium'
905 + elif total < high: level = 'high'
906 + else: level = 'very high'
907 + print(f"{pubkey}|{int(rx_rate)}|{int(tx_rate)}|{level}")
908 + else:
909 + print(f"{pubkey}|0|0|idle")
910 + else:
911 + print(f"{pubkey}|0|0|unknown")
912 +
913 + def remove_events_filtered(wg_file, fw_file, filter_name, filter_ip,
914 + filter_fw, filter_wg, before_days):
915 + """Remove events with filters: by name/ip, source, or age"""
916 + import time
917 + from datetime import datetime, timezone
918 +
919 + cutoff_ts = None
920 + if before_days:
921 + cutoff_ts = time.time() - (float(before_days) * 86400)
922 +
923 + def should_remove_wg(e):
924 + if filter_name and e.get('client') != filter_name:
925 + return False
926 + if cutoff_ts:
927 + try:
928 + ts = datetime.fromisoformat(e.get('timestamp','')).timestamp()
929 + return ts < cutoff_ts
930 + except:
931 + return False
932 + return True
933 +
934 + def should_remove_fw(e):
935 + if filter_ip and e.get('src_ip') != filter_ip:
936 + return False
937 + if cutoff_ts:
938 + try:
939 + ts = datetime.fromisoformat(e.get('timestamp','')).timestamp()
940 + return ts < cutoff_ts
941 + except:
942 + return False
943 + return True
944 +
945 + removed_wg = removed_fw = 0
946 +
947 + if not filter_fw and os.path.exists(wg_file):
948 + lines = []
949 + with open(wg_file) as f:
950 + for line in f:
951 + try:
952 + e = json.loads(line.strip())
953 + if should_remove_wg(e):
954 + removed_wg += 1
955 + continue
956 + except:
957 + pass
958 + lines.append(line)
959 + with open(wg_file, 'w') as f:
960 + f.writelines(lines)
961 +
962 + if not filter_wg and os.path.exists(fw_file):
963 + lines = []
964 + with open(fw_file) as f:
965 + for line in f:
966 + try:
967 + e = json.loads(line.strip())
968 + if should_remove_fw(e):
969 + removed_fw += 1
970 + continue
971 + except:
972 + pass
973 + lines.append(line)
974 + with open(fw_file, 'w') as f:
975 + f.writelines(lines)
976 +
977 + print(f"{removed_wg}|{removed_fw}")
978 +
979 + def _rule_resolve_internal(rules_dir, rule_name, visited=None):
980 + """Internal recursive resolver — returns dict, does not print"""
981 + if visited is None:
982 + visited = set()
983 + if rule_name in visited:
984 + raise ValueError(f"Circular dependency detected: {rule_name}")
985 + visited.add(rule_name)
986 +
987 + rule_file = find_rule_file(rules_dir, rule_name)
988 + with open(rule_file) as f:
989 + rule = json.load(f)
990 +
991 + merged = {
992 + 'allow_ips': [],
993 + 'allow_ports': [],
994 + 'block_ips': [],
995 + 'block_ports': [],
996 + 'dns_redirect': False
997 + }
998 +
999 + for base_name in rule.get('extends', []):
1000 + base = _rule_resolve_internal(rules_dir, base_name, visited.copy())
1001 + merged['allow_ips'] += base.get('allow_ips', [])
1002 + merged['allow_ports'] += base.get('allow_ports', [])
1003 + merged['block_ips'] += base.get('block_ips', [])
1004 + merged['block_ports'] += base.get('block_ports', [])
1005 + if base.get('dns_redirect'):
1006 + merged['dns_redirect'] = True
1007 +
1008 + # Merge own fields — use .get() with defaults for all fields
1009 + merged['allow_ips'] = list(dict.fromkeys(
1010 + merged['allow_ips'] + rule.get('allow_ips', [])))
1011 + merged['allow_ports'] = list(dict.fromkeys(
1012 + merged['allow_ports'] + rule.get('allow_ports', [])))
1013 + merged['block_ips'] = list(dict.fromkeys(
1014 + merged['block_ips'] + rule.get('block_ips', [])))
1015 + merged['block_ports'] = list(dict.fromkeys(
1016 + merged['block_ports'] + rule.get('block_ports', [])))
1017 + if rule.get('dns_redirect', False):
1018 + merged['dns_redirect'] = True
1019 +
1020 + merged['name'] = rule.get('name', rule_name)
1021 + merged['desc'] = rule.get('desc', '')
1022 + merged['group'] = rule.get('group', '')
1023 + merged['extends'] = rule.get('extends', [])
1024 + return merged
1025 +
1026 + def rule_resolve(rules_dir, rule_name):
1027 + """Resolve a rule with inheritance — prints JSON"""
1028 + try:
1029 + resolved = _rule_resolve_internal(rules_dir, rule_name)
1030 + print(json.dumps(resolved))
1031 + except Exception as e:
1032 + print(f"Error: {e}", file=sys.stderr)
1033 + sys.exit(1)
1034 +
1035 + def rule_resolve_field(rules_dir, rule_name, field):
1036 + """Get a single field from resolved rule — prints values one per line"""
1037 + try:
1038 + resolved = _rule_resolve_internal(rules_dir, rule_name)
1039 + val = resolved.get(field, [])
1040 + if isinstance(val, list):
1041 + for v in val:
1042 + print(v)
1043 + else:
1044 + print(val)
1045 + except Exception as e:
1046 + print(f"Error: {e}", file=sys.stderr)
1047 + sys.exit(1)
1048 +
1049 + def rule_inspect(rules_dir, rule_name):
1050 + """Show inheritance tree for a rule"""
1051 + try:
1052 + rule_file = find_rule_file(rules_dir, rule_name)
1053 + with open(rule_file) as f:
1054 + rule = json.load(f)
1055 + resolved = _rule_resolve_internal(rules_dir, rule_name)
1056 + has_extends = bool(rule.get('extends', []))
1057 +
1058 + # Own rules
1059 + for ip in rule.get('allow_ips', []):
1060 + print(f"own|allow_ip|{ip}")
1061 + for p in rule.get('allow_ports', []):
1062 + print(f"own|allow_port|{p}")
1063 + for ip in rule.get('block_ips', []):
1064 + print(f"own|block_ip|{ip}")
1065 + for p in rule.get('block_ports', []):
1066 + print(f"own|block_port|{p}")
1067 +
1068 + # DNS redirect — separate section
1069 + if rule.get('dns_redirect'):
1070 + print(f"dns|dns_redirect|true")
1071 +
1072 + if has_extends:
1073 + # Inherited rules per base
1074 + for base_name in rule.get('extends', []):
1075 + base = _rule_resolve_internal(rules_dir, base_name)
1076 + for ip in base.get('allow_ips', []):
1077 + print(f"inherited:{base_name}|allow_ip|{ip}")
1078 + for p in base.get('allow_ports', []):
1079 + print(f"inherited:{base_name}|allow_port|{p}")
1080 + for ip in base.get('block_ips', []):
1081 + print(f"inherited:{base_name}|block_ip|{ip}")
1082 + for p in base.get('block_ports', []):
1083 + print(f"inherited:{base_name}|block_port|{p}")
1084 + if base.get('dns_redirect'):
1085 + print(f"inherited:{base_name}|dns_redirect|true")
1086 +
1087 + # Resolved summary only when inheritance exists
1088 + has_resolved = (
1089 + resolved.get('allow_ips') or resolved.get('allow_ports') or
1090 + resolved.get('block_ips') or resolved.get('block_ports') or
1091 + resolved.get('dns_redirect')
1092 + )
1093 + if has_resolved:
1094 + for ip in resolved.get('allow_ips', []):
1095 + print(f"resolved|allow_ip|{ip}")
1096 + for p in resolved.get('allow_ports', []):
1097 + print(f"resolved|allow_port|{p}")
1098 + for ip in resolved.get('block_ips', []):
1099 + print(f"resolved|block_ip|{ip}")
1100 + for p in resolved.get('block_ports', []):
1101 + print(f"resolved|block_port|{p}")
1102 + if resolved.get('dns_redirect'):
1103 + print(f"resolved|dns_redirect|true")
1104 +
1105 + except Exception as e:
1106 + print(f"Error: {e}", file=sys.stderr)
1107 + sys.exit(1)
1108 +
1109 + def find_rule_file(rules_dir, rule_name):
1110 + """Find rule file in rules/ or rules/base/"""
1111 + for path in [
1112 + os.path.join(rules_dir, f"{rule_name}.rule"),
1113 + os.path.join(rules_dir, "base", f"{rule_name}.rule"),
1114 + ]:
1115 + if os.path.exists(path):
1116 + return path
1117 + return ""
1118 +
1119 + def get_raw(file, key):
1120 + try:
1121 + with open(file) as f:
1122 + data = json.load(f)
1123 + val = data.get(key) # returns None if missing
1124 + if val is None:
1125 + pass # print nothing
1126 + elif isinstance(val, bool):
1127 + print(str(val).lower())
1128 + elif isinstance(val, list):
1129 + for v in val:
1130 + print(v)
1131 + else:
1132 + print(val)
1133 + except:
1134 + pass
1135 +
1136 + def count_resolved(rules_dir, rule_name, key):
1137 + """Count entries in resolved rule field"""
1138 + try:
1139 + resolved = _rule_resolve_internal(rules_dir, rule_name)
1140 + print(len(resolved.get(key, [])))
1141 + except:
1142 + print(0)
1143 +
1144 + def _block_init(peer_ip):
1145 + """Return empty block structure"""
1146 + return {
1147 + "peer_ip": peer_ip,
1148 + "blocked_direct": False,
1149 + "blocked_by_groups": [],
1150 + "rules": []
1151 + }
1152 +
1153 + def _block_read(file):
1154 + try:
1155 + with open(file) as f:
1156 + content = f.read().strip()
1157 + if not content:
1158 + return None # empty file = no block data
1159 + try:
1160 + return json.loads(content)
1161 + except json.JSONDecodeError:
1162 + # Old format — migrate
1163 + lines = content.split('\n')
1164 + peer_ip = lines[0].split()[0] if lines else ''
1165 + new_data = {
1166 + "peer_ip": peer_ip,
1167 + "blocked_direct": True,
1168 + "blocked_by_groups": [],
1169 + "rules": [{"name": "full block", "type": "full"}]
1170 + }
1171 + with open(file, 'w') as f:
1172 + json.dump(new_data, f, indent=2)
1173 + return new_data
1174 + except FileNotFoundError:
1175 + return None
1176 + except Exception:
1177 + return None
1178 +
1179 + def _block_write(file, data):
1180 + """Write block file"""
1181 + with open(file, 'w') as f:
1182 + json.dump(data, f, indent=2)
1183 +
1184 + def block_get(file):
1185 + """Read and print block file as JSON"""
1186 + data = _block_read(file)
1187 + if data:
1188 + print(json.dumps(data))
1189 +
1190 + def block_is_blocked(file):
1191 + """Return true if peer is effectively blocked"""
1192 + data = _block_read(file)
1193 + if not data:
1194 + print("false")
1195 + return
1196 + blocked = data.get("blocked_direct", False) or \
1197 + bool(data.get("blocked_by_groups", []))
1198 + print("true" if blocked else "false")
1199 +
1200 + def block_set_direct(file, peer_ip, value):
1201 + """Set blocked_direct"""
1202 + try:
1203 + data = _block_read(file) or _block_init(peer_ip)
1204 + data["blocked_direct"] = value.lower() == "true"
1205 + data["peer_ip"] = peer_ip
1206 + _block_write(file, data)
1207 + remaining = data["blocked_direct"] or bool(data.get("blocked_by_groups", []))
1208 + pass
1209 + # print("true" if remaining else "false")
1210 + except Exception as e:
1211 + print(f"Error: {e}", file=sys.stderr)
1212 + sys.exit(1)
1213 +
1214 + def block_add_group(file, peer_ip, group):
1215 + """Add group to blocked_by_groups"""
1216 + try:
1217 + data = _block_read(file) or _block_init(peer_ip)
1218 + data["peer_ip"] = peer_ip
1219 + groups = data.get("blocked_by_groups", [])
1220 + if group not in groups:
1221 + groups.append(group)
1222 + data["blocked_by_groups"] = groups
1223 + _block_write(file, data)
1224 + except Exception as e:
1225 + print(f"Error: {e}", file=sys.stderr)
1226 + sys.exit(1)
1227 +
1228 + def block_remove_group(file, peer_ip, group):
1229 + """Remove group from blocked_by_groups, return whether still blocked"""
1230 + try:
1231 + data = _block_read(file) or _block_init(peer_ip)
1232 + groups = data.get("blocked_by_groups", [])
1233 + if group in groups:
1234 + groups.remove(group)
1235 + data["blocked_by_groups"] = groups
1236 + _block_write(file, data)
1237 + except Exception as e:
1238 + print(f"Error: {e}", file=sys.stderr)
1239 + sys.exit(1)
1240 + # print("true" if remaining else "false")
1241 + pass
1242 + except Exception as e:
1243 + print(f"Error: {e}", file=sys.stderr)
1244 + sys.exit(1)
1245 +
1246 + def block_add_rule(file, peer_ip, rule_type, name="", target="",
1247 + port="", proto=""):
1248 + """Add a block rule entry"""
1249 + try:
1250 + data = _block_read(file) or _block_init(peer_ip)
1251 + data["peer_ip"] = peer_ip
1252 + rule = {"type": rule_type}
1253 + if name: rule["name"] = name
1254 + if target: rule["target"] = target
1255 + if port: rule["port"] = port
1256 + if proto: rule["proto"] = proto
1257 +
1258 + rules = data.get("rules", [])
1259 + for existing in rules:
1260 + if existing.get("type") == rule_type and \
1261 + existing.get("target","") == target and \
1262 + existing.get("port","") == port and \
1263 + existing.get("proto","") == proto:
1264 + return # already exists, skip
1265 +
1266 + rules.append(rule)
1267 + data["rules"] = rules
1268 + _block_write(file, data)
1269 + except Exception as e:
1270 + print(f"Error: {e}", file=sys.stderr)
1271 + sys.exit(1)
1272 +
1273 + def block_remove_rule(file, rule_type, target="", port="", proto=""):
1274 + data = _block_read(file)
1275 + if not data:
1276 + return
1277 + rules = data.get("rules", [])
1278 + filtered = [r for r in rules if not (
1279 + r.get("type") == rule_type and
1280 + r.get("target", "") == target and
1281 + r.get("port", "") == port and
1282 + r.get("proto", "") == proto
1283 + )]
1284 + data["rules"] = filtered
1285 + _block_write(file, data)
1286 +
1287 + def block_get_rules(file):
1288 + """Print rules as pipe-separated lines: name|type|target|port|proto"""
1289 + data = _block_read(file)
1290 + if not data:
1291 + return
1292 + for r in data.get("rules", []):
1293 + print(f"{r.get('name','')}|{r.get('type','')}|"
1294 + f"{r.get('target','')}|{r.get('port','')}|{r.get('proto','')}")
1295 +
1296 + def block_get_groups(file):
1297 + data = _block_read(file)
1298 + if not data:
1299 + return
1300 + print(','.join(data.get('blocked_by_groups', [])))
1301 +
1302 + def block_get_direct(file):
1303 + data = _block_read(file)
1304 + if not data:
1305 + print('false')
1306 + return
1307 + print('true' if data.get('blocked_direct', False) else 'false')
1308 +
1309 + # ============================================
1310 + # Net / Services
1311 + # ============================================
1312 +
1313 + def _net_read(file):
1314 + """Read services.json, return dict or empty dict"""
1315 + try:
1316 + if not os.path.exists(file):
1317 + return {}
1318 + with open(file) as f:
1319 + content = f.read().strip()
1320 + if not content:
1321 + return {}
1322 + return json.loads(content)
1323 + except Exception:
1324 + return {}
1325 +
1326 + def _net_write(file, data):
1327 + """Write services.json"""
1328 + os.makedirs(os.path.dirname(file), exist_ok=True)
1329 + with open(file, 'w') as f:
1330 + json.dump(data, f, indent=2)
1331 +
1332 + def net_list(file):
1333 + """List all service names with IP and port count"""
1334 + data = _net_read(file)
1335 + for name, svc in sorted(data.items()):
1336 + ip = svc.get('ip', '')
1337 + desc = svc.get('desc', '')
1338 + tags = ','.join(svc.get('tags', []))
1339 + ports = len(svc.get('ports', {}))
1340 + print(f"{name}|{ip}|{desc}|{tags}|{ports}")
1341 +
1342 + def net_show(file, name):
1343 + """Show full service details"""
1344 + data = _net_read(file)
1345 + if name not in data:
1346 + print(f"Error: Service not found: {name}", file=sys.stderr)
1347 + sys.exit(1)
1348 + svc = data[name]
1349 + print(f"name|{name}")
1350 + print(f"ip|{svc.get('ip','')}")
1351 + print(f"desc|{svc.get('desc','')}")
1352 + print(f"tags|{','.join(svc.get('tags',[]))}")
1353 + for port_name, port_def in svc.get('ports', {}).items():
1354 + port = port_def.get('port', '')
1355 + proto = port_def.get('proto', 'tcp')
1356 + desc = port_def.get('desc', '')
1357 + print(f"port|{port_name}|{port}|{proto}|{desc}")
1358 +
1359 + def net_exists(file, name):
1360 + """Check if service exists"""
1361 + data = _net_read(file)
1362 + # Handle service:port syntax
1363 + if ':' in name:
1364 + svc_name, port_name = name.split(':', 1)
1365 + if port_name == 'ports':
1366 + print('true' if svc_name in data else 'false')
1367 + else:
1368 + svc = data.get(svc_name, {})
1369 + print('true' if port_name in svc.get('ports', {}) else 'false')
1370 + else:
1371 + print('true' if name in data else 'false')
1372 +
1373 + def net_add_service(file, name, ip, desc='', tags=''):
1374 + """Add or update a service"""
1375 + data = _net_read(file)
1376 + if name not in data:
1377 + data[name] = {'ip': ip, 'ports': {}}
1378 + else:
1379 + data[name]['ip'] = ip
1380 + if desc:
1381 + data[name]['desc'] = desc
1382 + if tags:
1383 + data[name]['tags'] = [t.strip() for t in tags.split(',') if t.strip()]
1384 + _net_write(file, data)
1385 +
1386 + def net_add_port(file, service, port_name, port, proto='tcp', desc=''):
1387 + """Add or update a port on a service"""
1388 + data = _net_read(file)
1389 + if service not in data:
1390 + print(f"Error: Service not found: {service}", file=sys.stderr)
1391 + sys.exit(1)
1392 + if 'ports' not in data[service]:
1393 + data[service]['ports'] = {}
1394 + entry = {'port': int(port), 'proto': proto}
1395 + if desc:
1396 + entry['desc'] = desc
1397 + data[service]['ports'][port_name] = entry
1398 + _net_write(file, data)
1399 +
1400 + def net_remove(file, name):
1401 + """Remove service or port"""
1402 + data = _net_read(file)
1403 + if ':' in name:
1404 + svc_name, port_name = name.split(':', 1)
1405 + if svc_name not in data:
1406 + print(f"Error: Service not found: {svc_name}", file=sys.stderr)
1407 + sys.exit(1)
1408 + if port_name == 'ports':
1409 + # Remove all ports
1410 + data[svc_name]['ports'] = {}
1411 + else:
1412 + if port_name not in data[svc_name].get('ports', {}):
1413 + print(f"Error: Port not found: {port_name}", file=sys.stderr)
1414 + sys.exit(1)
1415 + del data[svc_name]['ports'][port_name]
1416 + else:
1417 + if name not in data:
1418 + print(f"Error: Service not found: {name}", file=sys.stderr)
1419 + sys.exit(1)
1420 + del data[name]
1421 + _net_write(file, data)
1422 +
1423 + def net_resolve(file, name):
1424 + """Resolve service name to ip or ip:port:proto lines"""
1425 + data = _net_read(file)
1426 + if ':' in name:
1427 + svc_name, port_name = name.split(':', 1)
1428 + if svc_name not in data:
1429 + print(f"Error: Service not found: {svc_name}", file=sys.stderr)
1430 + sys.exit(1)
1431 + svc = data[svc_name]
1432 + ip = svc.get('ip', '')
1433 + if port_name == 'ports':
1434 + # All ports
1435 + for pname, pdef in svc.get('ports', {}).items():
1436 + print(f"{ip}:{pdef['port']}:{pdef.get('proto','tcp')}")
1437 + else:
1438 + if port_name not in svc.get('ports', {}):
1439 + print(f"Error: Port not found: {port_name}", file=sys.stderr)
1440 + sys.exit(1)
1441 + pdef = svc['ports'][port_name]
1442 + print(f"{ip}:{pdef['port']}:{pdef.get('proto','tcp')}")
1443 + else:
1444 + if name not in data:
1445 + print(f"Error: Service not found: {name}", file=sys.stderr)
1446 + sys.exit(1)
1447 + print(data[name].get('ip', ''))
1448 +
1449 + def net_reverse_lookup(file, ip, port='', proto=''):
1450 + """Reverse lookup IP/port to service name"""
1451 + data = _net_read(file)
1452 + for svc_name, svc in data.items():
1453 + if svc.get('ip') != ip:
1454 + continue
1455 + if not port:
1456 + print(svc_name)
1457 + return
1458 + for port_name, port_def in svc.get('ports', {}).items():
1459 + if (str(port_def.get('port','')) == str(port) and
1460 + port_def.get('proto','tcp') == proto):
1461 + print(f"{svc_name}:{port_name}")
1462 + return
1463 + # IP matched but no port match — return service name
1464 + print(svc_name)
1465 + return
1466 +
1467 + def block_is_empty(file):
1468 + data = _block_read(file)
1469 + if not data:
1470 + print("true")
1471 + return
1472 + empty = (
1473 + not data.get("blocked_direct", False) and
1474 + not data.get("blocked_by_groups", []) and
1475 + not data.get("rules", []) and
1476 + not data.get("services", [])
1477 + )
1478 + print("true" if empty else "false")
1479 +
1480 + def group_has_peer(file, peer_name):
1481 + try:
1482 + with open(file) as f:
1483 + data = json.load(f)
1484 + peers = data.get('peers', [])
1485 + print('true' if peer_name in peers else 'false')
1486 + except Exception:
1487 + print('false')
1488 +
1489 + commands = {
1490 + 'get': lambda args: get(args[0], args[1]),
1491 + 'set': lambda args: set_key(args[0], args[1], args[2]),
1492 + 'delete': lambda args: delete_key(args[0], args[1]),
1493 + 'append': lambda args: append(args[0], args[1], args[2]),
1494 + 'remove': lambda args: remove_value(args[0], args[1], args[2]),
1495 + 'cat': lambda args: cat(args[0]),
1496 + 'has_key': lambda args: has_key(args[0], args[1]),
1497 + 'filter_values': lambda args: filter_values(args[0], args[1], args[2]),
1498 + 'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]),
1499 + 'events_for': lambda args: events_for(args[0], args[1], args[2]),
1500 + 'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]),
1501 + 'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]),
1502 + 'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]),
1503 + 'format_wg_event': lambda args: format_wg_event(sys.stdin.read()),
1504 + 'remove_events': lambda args: remove_events(args[0], args[1]),
1505 + 'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4], args[5]),
1506 + 'count': lambda args: count(args[0], args[1]),
1507 + 'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
1508 + 'peer_group_map': lambda args: peer_group_map(args[0]),
1509 + 'peer_groups': lambda args: peer_groups(args[0], args[1]),
1510 + 'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
1511 + 'iso_to_ts': lambda args: iso_to_ts(args[0]),
1512 + 'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
1513 + 'group_list_data': lambda args: group_list_data(args[0], args[1]),
1514 + 'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]),
1515 + 'create_rule': lambda args: create_rule(
1516 + args[0], args[1], args[2], args[3], args[4], args[5], args[6],
1517 + args[7] if len(args) > 7 else '',
1518 + args[8] if len(args) > 8 else '',
1519 + args[9] if len(args) > 9 else ''
1520 + ),
1521 + 'cleanup_config': lambda args: cleanup_config(args[0]),
1522 + 'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
1523 + 'create_group': lambda args: create_group(args[0], args[1], args[2]),
1524 + 'parse_event': lambda args: parse_event(args[0]),
1525 + 'parse_fw_event': lambda args: parse_fw_event(args[0]),
1526 + 'peer_transfer': lambda args: peer_transfer(args[0]),
1527 + 'remove_events_filtered': lambda args: remove_events_filtered(
1528 + args[0], args[1], args[2], args[3], args[4]=='true', args[5]=='true', args[6] if len(args)>6 else ''),
1529 + 'peer_transfer': lambda args: peer_transfer(args[0]),
1530 + 'peer_transfer_delta': lambda args: peer_transfer_delta(args[0], args[1]),
1531 + 'rule_resolve': lambda args: rule_resolve(args[0], args[1]),
1532 + 'rule_resolve_field': lambda args: rule_resolve_field(args[0], args[1], args[2]),
1533 + 'rule_inspect': lambda args: rule_inspect(args[0], args[1]),
1534 + 'find_rule_file': lambda args: print(find_rule_file(args[0], args[1])),
1535 + 'get_raw': lambda args: print(get_raw(args[0], args[1])),
1536 + 'count_resolved': lambda args: count_resolved(args[0], args[1], args[2]),
1537 + 'block_get': lambda args: block_get(args[0]),
1538 + 'block_is_blocked': lambda args: block_is_blocked(args[0]),
1539 + 'block_set_direct': lambda args: block_set_direct(args[0], args[1], args[2]),
1540 + 'block_add_group': lambda args: block_add_group(args[0], args[1], args[2]),
1541 + 'block_remove_group': lambda args: block_remove_group(args[0], args[1], args[2]),
1542 + 'block_add_rule': lambda args: block_add_rule(
1543 + args[0], args[1], args[2],
1544 + args[3] if len(args) > 3 else '',
1545 + args[4] if len(args) > 4 else '',
1546 + args[5] if len(args) > 5 else '',
1547 + args[6] if len(args) > 6 else ''
1548 + ),
1549 + 'block_remove_rule': lambda args: block_remove_rule(
1550 + args[0], args[1],
1551 + args[2] if len(args) > 2 else '',
1552 + args[3] if len(args) > 3 else '',
1553 + args[4] if len(args) > 4 else ''
1554 + ),
1555 + 'block_get_rules': lambda args: block_get_rules(args[0]),
1556 + 'block_get_groups': lambda args: block_get_groups(args[0]),
1557 + 'block_get_direct': lambda args: block_get_direct(args[0]),
1558 + 'net_list': lambda args: net_list(args[0]),
1559 + 'net_show': lambda args: net_show(args[0], args[1]),
1560 + 'net_exists': lambda args: net_exists(args[0], args[1]),
1561 + 'net_add_service': lambda args: net_add_service(
1562 + args[0], args[1], args[2],
1563 + args[3] if len(args) > 3 else '',
1564 + args[4] if len(args) > 4 else ''
1565 + ),
1566 + 'net_add_port': lambda args: net_add_port(
1567 + args[0], args[1], args[2], args[3],
1568 + args[4] if len(args) > 4 else 'tcp',
1569 + args[5] if len(args) > 5 else ''
1570 + ),
1571 + 'net_remove': lambda args: net_remove(args[0], args[1]),
1572 + 'net_resolve': lambda args: net_resolve(args[0], args[1]),
1573 + 'net_reverse_lookup': lambda args: net_reverse_lookup(
1574 + args[0], args[1],
1575 + args[2] if len(args) > 2 else '',
1576 + args[3] if len(args) > 3 else ''
1577 + ),
1578 + 'block_is_empty': lambda args: block_is_empty(args[0]),
1579 + 'group_has_peer': lambda args: group_has_peer(args[0], args[1]),
1580 + }
1581 +
1582 + if __name__ == '__main__':
1583 + if len(sys.argv) < 2:
1584 + print("Usage: json_helper.py <command> <file> [key] [value]", file=sys.stderr)
1585 + sys.exit(1)
1586 +
1587 + cmd = sys.argv[1]
1588 + args = sys.argv[2:]
1589 +
1590 + if cmd not in commands:
1591 + print(f"Unknown command: {cmd}", file=sys.stderr)
1592 + sys.exit(1)
1593 +
1594 + commands[cmd](args)
Новее Позже