cleanup structure,bak files | change peers header to match fw in rule inspect

This commit is contained in:
Nuno Duque Nunes 2026-05-19 01:17:20 +00:00
parent 7323bf20f1
commit 4ac25e283d
7 changed files with 8 additions and 2047 deletions

View file

@ -1,254 +0,0 @@
#!/usr/bin/env bash
FW_EVENTS_LOG="$(ctx::fw_events_log)"
WG_EVENTS_LOG="$(ctx::events_log)"
function cmd::logs::on_load() {
flag::register --name
flag::register --type
flag::register --since
flag::register --limit
flag::register --fw
flag::register --wg
flag::register --follow
}
function cmd::logs::help() {
cat <<EOF
Usage: wgctl logs [options]
Show WireGuard and firewall activity logs.
Options:
--name <name> Filter by client name
--type <type> Filter by device type
--since <time> Time filter (e.g. 1h, 24h, 7d)
--limit <n> Max results per source (default 50)
--fw Show only firewall drops
--wg Show only WireGuard events
Examples:
wgctl logs
wgctl logs --name guest-test
wgctl logs --type guest
wgctl logs --since 1h
wgctl logs --fw --limit 100
EOF
}
function cmd::logs::run() {
local subcmd="${1:-show}"
# Check if first arg is a flag
if [[ "$subcmd" == --* ]]; then
subcmd="show"
else
shift || true
fi
case "$subcmd" in
show) cmd::logs::show "$@" ;;
remove|rm|del) cmd::logs::remove "$@" ;;
help) cmd::logs::help ;;
*)
log::error "Unknown subcommand: '${subcmd}'"
cmd::logs::help
return 1
;;
esac
}
function cmd::logs::show() {
local name="" type="" since="" limit=50
local fw_only=false wg_only=false follow=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--type) type="$2"; shift 2 ;;
--since) since="$2"; shift 2 ;;
--limit) limit="$2"; shift 2 ;;
--fw) fw_only=true; shift ;;
--wg) wg_only=true; shift ;;
--follow|-f) follow=true; shift ;;
--help) cmd::logs::help; return ;;
*)
log::error "Unknown flag: $1"
return 1
;;
esac
done
if [[ -n "$name" && -n "$type" ]]; then
name=$(peers::resolve_and_require "$name" "$type") || return 1
fi
local filter_ip=""
if [[ -n "$name" ]]; then
filter_ip=$(peers::get_ip "$name")
[[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1
fi
if $follow; then
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
return
fi
log::section "WireGuard Activity Log"
printf "\n"
$wg_only || cmd::logs::show_fw_events "$filter_ip" "$name" "$type" "$limit"
$fw_only || cmd::logs::show_wg_events "$filter_ip" "$name" "$type" "$limit"
}
function cmd::logs::follow() {
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}"
local fw_only="${4:-false}" wg_only="${5:-false}"
local filter_peers="${6:-}"
local clients_dir
clients_dir="$(ctx::clients)"
local wg_log="$WG_EVENTS_LOG"
local fw_log="$FW_EVENTS_LOG"
$fw_only && wg_log=""
$wg_only && fw_log=""
log::section "WireGuard Live Log (Ctrl+C to stop)"
printf "\n %-20s %-8s %-20s %-25s %s\n" \
"TIME" "SOURCE" "CLIENT" "DESTINATION/ENDPOINT" "EVENT"
printf " %s\n" "$(printf '─%.0s' {1..90})"
while IFS="|" read -r source ts client dst_or_endpoint event; do
if [[ "$source" == "fw" ]]; then
local colored_event
case "$event" in
tcp) colored_event="\033[1;33mtcp\033[0m" ;;
udp) colored_event="\033[0;36mudp\033[0m" ;;
icmp) colored_event="\033[0;37micmp\033[0m" ;;
*) colored_event="$event" ;;
esac
printf " %-20s %-8s %-20s %-25s %b\n" \
"$ts" "firewall" "$client" "$dst_or_endpoint" "$colored_event"
else
local colored_event
case "$event" in
attempt) colored_event="\033[1;31mattempt\033[0m" ;;
handshake) colored_event="\033[1;32mhandshake\033[0m" ;;
*) colored_event="$event" ;;
esac
printf " %-20s %-8s %-20s %-25s %b\n" \
"$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event"
fi
done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" "$clients_dir" "$filter_peers")
}
function cmd::logs::remove() {
local name="" type="" force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--type) type="$2"; shift 2 ;;
--force) force=true; shift ;;
--help) cmd::logs::help; return ;;
*)
log::error "Unknown flag: $1"
return 1
;;
esac
done
if [[ -z "$name" ]]; then
log::error "Missing required flag: --name"
return 1
fi
name=$(peers::resolve_and_require "$name" "$type") || return 1
local client_ip
client_ip=$(peers::get_ip "$name")
local before_wg before_fw after_wg after_fw
before_wg=$(wc -l < "$WG_EVENTS_LOG" 2>/dev/null || echo 0)
before_fw=$(wc -l < "$FW_EVENTS_LOG" 2>/dev/null || echo 0)
json::remove_events "$WG_EVENTS_LOG" "$name"
json::remove_events "$FW_EVENTS_LOG" "$client_ip"
after_wg=$(wc -l < "$WG_EVENTS_LOG" 2>/dev/null || echo 0)
after_fw=$(wc -l < "$FW_EVENTS_LOG" 2>/dev/null || echo 0)
local removed=$(( (before_wg - after_wg) + (before_fw - after_fw) ))
if [[ "$removed" -eq 0 ]]; then
log::wg_warning "No log entries found for: ${name}"
return 0
fi
if ! $force; then
read -r -p "Remove all log entries for '${name}'? [y/N] " confirm
case "$confirm" in
[yY][eE][sS]|[yY]) ;;
*) log::info "Aborted"; return 0 ;;
esac
fi
log::wg_success "Removed ${removed} log entries for: ${name}"
json::remove_events "$WG_EVENTS_LOG" "$name"
json::remove_events "$FW_EVENTS_LOG" "$client_ip"
log::wg_success "Removed log entries for: ${name}"
}
function cmd::logs::show_wg_events() {
local filter_ip="$1" filter_name="$2" filter_type="$3" limit="$4"
[[ ! -f "$WG_EVENTS_LOG" ]] && return 0
printf " WireGuard Events:\n"
printf " %-20s %-20s %-18s %s\n" "TIME" "CLIENT" "ENDPOINT" "EVENT"
printf " %s\n" "$(printf '─%.0s' {1..75})"
local found=false
while IFS="|" read -r ts client endpoint event; do
[[ -z "$ts" ]] && continue
local colored_event
case "$event" in
attempt) colored_event="\033[1;31mattempt\033[0m" ;;
handshake) colored_event="\033[1;32mhandshake\033[0m" ;;
*) colored_event="$event" ;;
esac
printf " %-20s %-20s %-18s %b\n" "$ts" "$client" "$endpoint" "$colored_event"
found=true
done < <(json::wg_events "$WG_EVENTS_LOG" "$filter_name" "$filter_type" "$limit")
$found || printf " —\n"
printf "\n"
}
function cmd::logs::show_fw_events() {
local filter_ip="$1" filter_name="$2" filter_type="$3" limit="$4"
[[ ! -f "$FW_EVENTS_LOG" ]] && return 0
printf " Firewall Drops:\n"
printf " %-20s %-18s %-25s %s\n" "TIME" "CLIENT" "DESTINATION" "PROTOCOL"
printf " %s\n" "$(printf '─%.0s' {1..75})"
local found=false
while IFS="|" read -r ts client dst proto; do
[[ -z "$ts" ]] && continue
local colored_proto
case "$proto" in
tcp) colored_proto="\033[1;33mtcp\033[0m" ;;
udp) colored_proto="\033[1;36mudp\033[0m" ;;
icmp) colored_proto="\033[0;37micmp\033[0m" ;;
*) colored_proto="$proto" ;;
esac
printf " %-20s %-18s %-25s %b\n" "$ts" "$client" "$dst" "$colored_proto"
found=true
done < <(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" "$(ctx::clients)" "$limit")
$found || printf " —\n"
printf "\n"
}

View file

@ -313,215 +313,6 @@ function cmd::rule::list() {
# Show
# ============================================
# function cmd::rule::show() {
# local name="" show_peers=true color=false show_resolved=false
# while [[ $# -gt 0 ]]; do
# case "$1" in
# --name) util::require_flag "--name" "${2:-}" || return 1
# name="$2"; shift 2 ;;
# --no-peers) show_peers=false; shift ;;
# --color) color=true; shift ;;
# --resolved) show_resolved=true; shift ;;
# --help) cmd::rule::help; return ;;
# *) log::error "Unknown flag: $1"; return 1 ;;
# esac
# done
# [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
# rule::require_exists "$name" || return 1
# local rule_file
# rule_file="$(rule::path "$name")"
# local dns_redirect
# dns_redirect=$(rule::get_own "$name" "dns_redirect")
# dns_redirect="${dns_redirect:-false}"
# local resolved_dns
# resolved_dns=$(rule::get "$name" "dns_redirect")
# resolved_dns="${resolved_dns:-false}"
# local dns_display
# if [[ "${resolved_dns,,}" == "true" && "${dns_redirect,,}" != "true" ]]; then
# dns_display="true (inherited)"
# elif [[ "${dns_redirect,,}" == "true" ]]; then
# dns_display="true"
# else
# dns_display="false"
# fi
# log::section "Rule: ${name}"
# printf "\n"
# # ── Header info ──────────────────────────────
# local desc group dns_redirect
# desc=$(json::get "$rule_file" "desc")
# group=$(json::get "$rule_file" "group")
# ui::row "Description" "${desc:-—}"
# ui::row "Group" "${group:-—}"
# ui::row "DNS" "$dns_display"
# # ── Extends section ──────────────────────────
# local extends_raw=()
# mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true)
# if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]}" ]]; then
# cmd::rule::_show_section "Extends"
# for base_name in "${extends_raw[@]}"; do
# [[ -z "$base_name" ]] && continue
# printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name"
# # Show resolved entries for this base
# local base_allow_ports base_allow_ips base_block_ips base_block_ports base_dns
# base_allow_ports=$(rule::get "$base_name" "allow_ports" 2>/dev/null || true)
# base_allow_ips=$(rule::get "$base_name" "allow_ips" 2>/dev/null || true)
# base_block_ips=$(rule::get "$base_name" "block_ips" 2>/dev/null || true)
# base_block_ports=$(rule::get "$base_name" "block_ports" 2>/dev/null || true)
# base_dns=$(json::get "$(rule::path "$base_name")" "dns_redirect" 2>/dev/null || true)
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "+" "$e"
# done <<< "$base_allow_ports"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "-" "$e"
# done <<< "$base_allow_ips"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "-" "$e"
# done <<< "$base_block_ips"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "-" "$e"
# done <<< "$base_block_ports"
# [[ "$base_dns" == "true" ]] && \
# printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)"
# done
# fi
# # ── Own Rules section ─────────────────────────
# local own_allow_ports own_allow_ips own_block_ips own_block_ports
# own_allow_ports=$(json::get "$rule_file" "allow_ports")
# own_allow_ips=$(json::get "$rule_file" "allow_ips")
# own_block_ips=$(json::get "$rule_file" "block_ips")
# own_block_ports=$(json::get "$rule_file" "block_ports")
# local has_own=false
# [[ -n "$own_allow_ports" || -n "$own_allow_ips" || \
# -n "$own_block_ips" || -n "$own_block_ports" ]] && has_own=true
# if $has_own; then
# if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]}" ]]; then
# cmd::rule::_show_section "Own Rules"
# printf "\n"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "+" "$e"
# done <<< "$own_allow_ports"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "+" "$e"
# done <<< "$own_allow_ips"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "-" "$e"
# done <<< "$own_block_ips"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# net::print_entry "-" "$e"
# done <<< "$own_block_ports"
# [[ "$dns_redirect" == "true" ]] && \
# printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)"
# else
# # No inheritance — use Allow/Block sections
# if [[ -n "$own_allow_ports" || -n "$own_allow_ips" ]]; then
# cmd::rule::_show_section "Allow"
# printf "\n"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# printf " \033[0;32m+\033[0m %s\n" "$e"
# done <<< "$own_allow_ports"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# printf " \033[0;32m+\033[0m %s\n" "$e"
# done <<< "$own_allow_ips"
# fi
# if [[ -n "$own_block_ips" || -n "$own_block_ports" ]]; then
# cmd::rule::_show_section "Block"
# printf "\n"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# printf " \033[0;31m-\033[0m %s\n" "$e"
# done <<< "$own_block_ips"
# while IFS= read -r e; do
# [[ -z "$e" ]] && continue
# printf " \033[0;31m-\033[0m %s\n" "$e"
# done <<< "$own_block_ports"
# fi
# if [[ "$dns_redirect" == "true" ]]; then
# cmd::rule::_show_section "DNS"
# printf "\n \033[0;36m↺\033[0m Redirect all DNS → %s\n" "$(config::dns)"
# fi
# fi
# elif [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]}" ]]; then
# printf "\n"
# ui::row "Access" "full (no restrictions)"
# fi
# # ── Resolved section (optional) ──────────────
# if $show_resolved; then
# cmd::rule::_show_section "Resolved (applied to peers)"
# local res_allow_ports res_allow_ips res_block_ips res_block_ports
# res_allow_ports=$(rule::get "$name" "allow_ports")
# res_allow_ips=$(rule::get "$name" "allow_ips")
# res_block_ips=$(rule::get "$name" "block_ips")
# res_block_ports=$(rule::get "$name" "block_ports")
# while IFS= read -r e; do [[ -n "$e" ]] && \
# printf " \033[0;32m+\033[0m %s\n" "$e"; done <<< "$res_allow_ports"
# while IFS= read -r e; do [[ -n "$e" ]] && \
# printf " \033[0;32m+\033[0m %s\n" "$e"; done <<< "$res_allow_ips"
# while IFS= read -r e; do [[ -n "$e" ]] && \
# printf " \033[0;31m-\033[0m %s\n" "$e"; done <<< "$res_block_ips"
# while IFS= read -r e; do [[ -n "$e" ]] && \
# printf " \033[0;31m-\033[0m %s\n" "$e"; done <<< "$res_block_ports"
# fi
# # ── Peers section ─────────────────────────────
# cmd::rule::_show_section "Peers"
# local peer_list=()
# mapfile -t peer_list < <(peers::with_rule "$name")
# local peer_count=${#peer_list[@]}
# ui::row "Assigned" "$peer_count"
# if $show_peers && [[ $peer_count -gt 0 ]]; then
# printf "\n"
# for peer_name in "${peer_list[@]}"; do
# local ip
# ip=$(peers::get_ip "$peer_name")
# printf " %-28s %s\n" "$peer_name" "$ip"
# done
# fi
# printf "\n"
# }
function cmd::rule::show() {
local name="" show_peers=true show_resolved=false
@ -589,16 +380,21 @@ function cmd::rule::show() {
<<< "$res_allow_ports"$'\n'"$res_allow_ips"
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \
<<< "$res_block_ips"$'\n'"$res_block_ports"
printf "\n"
fi
# ── Peers ─────────────────────────────────────
cmd::rule::_show_section "Peers"
local peer_list=()
mapfile -t peer_list < <(peers::with_rule "$name")
local peer_count=${#peer_list[@]}
ui::row "Assigned" "$peer_count"
printf "\033[0;37m── Peers (%s) \033[0m%s\n\n" \
"$(color::gray "${peer_count}")" \
"$(printf '\033[0;37m─%.0s' {1..35})"
if $show_peers && [[ $peer_count -gt 0 ]]; then
printf "\n"
for peer_name in "${peer_list[@]}"; do
local ip
ip=$(peers::get_ip "$peer_name")

View file

@ -1,164 +0,0 @@
#!/usr/bin/env bash
# ============================================
# Private helpers
# ============================================
function cmd::shell::_prompt() {
local user host dir
user=$(whoami)
host=$(hostname -s)
dir=$(basename "$PWD")
printf "\033[1;32m%s@%s\033[0m:\033[0;36m%s\033[0m \033[1;34mwgctl\033[0m> " \
"$user" "$host" "$dir"
}
function cmd::shell::_is_wgctl_command() {
local cmd="$1"
# Check against known wgctl commands
local known=(
list add remove rm inspect block unblock
rule group audit logs watch fw config qr
rename keys ip service shell help
)
local c
for c in "${known[@]}"; do
[[ "$c" == "$cmd" ]] && return 0
done
return 1
}
function cmd::shell::_handle_builtin() {
local input="$1"
local first="${input%% *}"
case "$first" in
cd)
local dir="${input#cd }"
[[ "$dir" == "$input" ]] && dir="$HOME"
cd "$dir" 2>/dev/null || log::error "cd: $dir: No such file or directory"
return 0
;;
cd*) eval "$input" ;; # eval preserves shell state for cd
export|unset|source|.)
eval "$input"
return 0
;;
esac
return 1 # not a builtin
}
function cmd::shell::_execute() {
local input="$1"
local first="${input%% *}"
local rest="${input#"$first"}"
rest="${rest# }"
# Handle shell builtins first
cmd::shell::_handle_builtin "$input" && return 0
# Try as wgctl command via dispatcher
if cmd::shell::_is_wgctl_command "$first"; then
if [[ -n "$rest" ]]; then
wgctl::dispatch "$first" $rest || true # never exit REPL on failure
else
wgctl::dispatch "$first" || true
fi
return 0 # Always 0 to keep REPL running
fi
# Fall back to bash
bash -c "$input" || true # same for bash commands
}
function cmd::shell::_setup_history() {
HISTFILE="${HOME}/.wgctl_history"
HISTSIZE=1000
HISTFILESIZE=2000
history -r 2>/dev/null || true
}
function cmd::shell::_save_history() {
history -w 2>/dev/null || true
}
function cmd::shell::_banner() {
ui::section "wgctl shell"
printf "\n"
printf " Type wgctl commands directly, or any bash command.\n"
printf " Type \033[1mexit\033[0m or \033[1mquit\033[0m to leave.\n"
printf " Type \033[1mhelp\033[0m for wgctl commands.\n"
printf "\n"
}
# ============================================
# Run
# ============================================
function cmd::shell::on_load() {
: # no flags needed
}
function cmd::shell::help() {
cat <<EOF
Usage: wgctl shell
Start an interactive wgctl shell. Supports all wgctl commands
directly (no 'wgctl' prefix needed), plus any bash command.
Builtins handled: cd, export, unset, source
Everything else: passed to bash
Examples:
wgctl shell
wgctl> list
wgctl> inspect --name phone-nuno
wgctl> ls /etc/wireguard
wgctl> exit
EOF
}
function cmd::shell::run() {
cmd::shell::_banner
cmd::shell::_setup_history
while true; do
local input
# Read with readline support (-e) and custom prompt
IFS= read -r -e -p "$(cmd::shell::_prompt)" input || break
# Handle empty input
[[ -z "${input// }" ]] && continue
# Add to history
history -s "$input"
# Handle exit
case "${input%% *}" in
exit|quit) break ;;
esac
# Execute
cmd::shell::_execute "$input"
done
cmd::shell::_save_history
printf "\n Goodbye!\n\n"
}
# ============================================
# Tab completion (loaded when shell starts)
# ============================================
function cmd::shell::_setup_completion() {
local commands="list add remove inspect block unblock rule group audit logs watch fw config qr rename shell help"
function _wgctl_shell_complete() {
local cur="${COMP_WORDS[COMP_CWORD]}"
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
}
# Bind completion to the read prompt
bind 'set show-all-if-ambiguous on' 2>/dev/null || true
bind 'set completion-ignore-case on' 2>/dev/null || true
}

View file

@ -1,790 +0,0 @@
#!/usr/bin/env python3
"""
wgctl JSON helper called by shell functions to read/write JSON files.
Usage: json_helper.py <command> <file> [key] [value]
"""
import sys
import json
import os
DATETIME_FMT = os.environ.get('WGCTL_DATETIME_FMT', '%Y-%m-%d %H:%M')
def get(file, key):
try:
with open(file) as f:
data = json.load(f)
val = data.get(key, [])
if isinstance(val, bool):
print(str(val).lower()) # true/false not True/False
elif isinstance(val, list):
if val:
print('\n'.join(str(v) for v in val))
else:
if val:
print(val)
except:
sys.exit(0)
def set_key(file, key, value):
try:
data = {}
if os.path.exists(file):
with open(file) as f:
data = json.load(f)
# Try to parse as JSON value first (for arrays/bools)
try:
data[key] = json.loads(value)
except:
data[key] = value
with open(file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def delete_key(file, key):
try:
with open(file) as f:
data = json.load(f)
data.pop(key, None)
with open(file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def append(file, key, value):
try:
data = {}
if os.path.exists(file):
with open(file) as f:
data = json.load(f)
if key not in data:
data[key] = []
if value not in data[key]:
data[key].append(value)
with open(file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def remove_value(file, key, value):
try:
with open(file) as f:
data = json.load(f)
if key in data and value in data[key]:
data[key].remove(value)
with open(file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def cat(file):
try:
with open(file) as f:
data = json.load(f)
print(json.dumps(data, indent=2))
except Exception as e:
sys.exit(1)
def has_key(file, key):
try:
with open(file) as f:
data = json.load(f)
sys.exit(0 if key in data else 1)
except:
sys.exit(1)
def filter_values(file, key, value):
"""Remove all entries where value matches"""
try:
with open(file) as f:
data = json.load(f)
data = {k: v for k, v in data.items() if v != value}
with open(file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def last_event(file, key, field, client):
"""Get last event field for a client"""
try:
last = None
with open(file) as f:
for line in f:
try:
e = json.loads(line.strip())
if e.get(key) == client:
last = e
except:
pass
if last:
print(last.get(field, ''))
except:
pass
def events_for(file, ip, limit):
"""Format events for a given IP"""
try:
from datetime import datetime
events = []
with open(file) as f:
for line in f:
try:
e = json.loads(line.strip())
if e.get('ip') == ip:
events.append(e)
except:
pass
for e in events[-int(limit):]:
ts = e.get('timestamp', '')
try:
dt = datetime.fromisoformat(ts)
ts = dt.strftime(DATETIME_FMT)
except:
pass
endpoint = e.get('endpoint', '')
client = e.get('client', '')
event = e.get('event', '')
print(f' {ts} {client:<20} {endpoint:<20} {event}')
except:
pass
def fw_events(file, filter_ip, filter_type, clients_dir, limit):
"""Format firewall drop events"""
import glob
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
# Build ip->name map
ip_to_name = {}
for conf in glob.glob(f"{clients_dir}/*.conf"):
name = os.path.basename(conf).replace('.conf', '')
try:
with open(conf) as f:
for line in f:
if line.startswith('Address'):
ip = line.split('=')[1].strip().split('/')[0]
ip_to_name[ip] = name
except:
pass
events = []
last_seen = {} # (src, dst, port, proto) -> last timestamp
try:
with open(file) as f:
for line in f:
try:
e = json.loads(line.strip())
src = e.get('src_ip', '')
if not src:
continue
if filter_ip and src != filter_ip:
continue
# Dedup key
dst = e.get('dest_ip', '')
port = e.get('dest_port', '')
proto = e.get('ip.protocol', 0)
key = (src, dst, port, proto)
ts_str = e.get('timestamp', '')
try:
from datetime import datetime
ts = datetime.fromisoformat(ts_str).timestamp()
except:
ts = 0
# Protocol-aware dedup window
dedup_windows = {1: 5, 6: 30, 17: 10} # icmp=5s, tcp=30s, udp=10s
window = dedup_windows.get(proto, 10)
# Skip if same event within protocol window
if key in last_seen and (ts - last_seen[key]) < window:
continue
last_seen[key] = ts
events.append(e)
except:
pass
except:
pass
for e in events[-int(limit):]:
ts = e.get('timestamp', '')
try:
from datetime import datetime
dt = datetime.fromisoformat(ts)
ts = dt.strftime(DATETIME_FMT)
except:
pass
src = e.get('src_ip', '')
dst = e.get('dest_ip', '')
port = e.get('dest_port', '')
proto_num = e.get('ip.protocol', 0)
proto = proto_map.get(proto_num, str(proto_num))
dst_str = f"{dst}:{port}" if port else dst
client = ip_to_name.get(src, src)
if filter_type and not client.startswith(filter_type + '-'):
continue
print(f"{ts}|{client}|{dst_str}|{proto}")
def wg_events(file, filter_client, filter_type, limit):
"""Format WireGuard events from events.log"""
events = []
try:
with open(file) as f:
for line in f:
try:
e = json.loads(line.strip())
client = e.get('client', '')
if not client:
continue
if filter_client and client != filter_client:
continue
if filter_type and not client.startswith(filter_type + '-'):
continue
events.append(e)
except:
pass
except:
pass
for e in events[-int(limit):]:
ts = e.get('timestamp', '')
try:
from datetime import datetime
dt = datetime.fromisoformat(ts)
ts = dt.strftime(DATETIME_FMT)
except:
pass
client = e.get('client', '')
endpoint = e.get('endpoint', '')
event = e.get('event', '')
print(f"{ts}|{client}|{endpoint}|{event}")
def format_fw_event(line, clients_dir):
"""Format a single fw_event line"""
import glob
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
# Build ip->name map
ip_to_name = {}
for conf in glob.glob(f"{clients_dir}/*.conf"):
name = os.path.basename(conf).replace('.conf', '')
try:
with open(conf) as f:
for l in f:
if l.startswith('Address'):
ip = l.split('=')[1].strip().split('/')[0]
ip_to_name[ip] = name
except:
pass
try:
e = json.loads(line.strip())
src = e.get('src_ip', '')
if not src:
return None
ts = e.get('timestamp', '')
try:
from datetime import datetime
dt = datetime.fromisoformat(ts)
ts = dt.strftime(DATETIME_FMT)
except:
pass
dst = e.get('dest_ip', '')
port = e.get('dest_port', '')
proto_num = e.get('ip.protocol', 0)
proto = proto_map.get(proto_num, str(proto_num))
dst_str = f"{dst}:{port}" if port else dst
client = ip_to_name.get(src, src)
return f"{ts}|{client}|{dst_str}|{proto}"
except:
return None
def format_wg_event(line):
"""Format a single wg_event line"""
try:
e = json.loads(line.strip())
client = e.get('client', '')
if not client:
return None
ts = e.get('timestamp', '')
try:
from datetime import datetime
dt = datetime.fromisoformat(ts)
ts = dt.strftime(DATETIME_FMT)
except:
pass
endpoint = e.get('endpoint', '')
event = e.get('event', '')
return f"{ts}|{client}|{endpoint}|{event}|wg"
except:
return None
def remove_events(file, identifier):
"""Remove all events for a client/ip from a JSONL file"""
try:
lines = []
with open(file) as f:
for line in f:
try:
e = json.loads(line.strip())
if e.get('client') == identifier or e.get('src_ip') == identifier:
continue
lines.append(line)
except:
lines.append(line)
with open(file, 'w') as f:
f.writelines(lines)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_peers=""):
"""Follow both log files and output formatted events"""
import glob, time, select
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
peer_filter = set(filter_peers.split(',')) if filter_peers else set()
import sys
print(f"DEBUG follow_logs: peer_filter={peer_filter}", file=sys.stderr, flush=True)
# Build ip->name map
ip_to_name = {}
for conf in glob.glob(f"{clients_dir}/*.conf"):
name = os.path.basename(conf).replace('.conf', '')
try:
with open(conf) as f:
for l in f:
if l.startswith('Address'):
ip = l.split('=')[1].strip().split('/')[0]
ip_to_name[ip] = name
except:
pass
# Open files and seek to end
files = {}
for label, path in [('fw', fw_file), ('wg', wg_file)]:
if path and os.path.exists(path):
f = open(path)
f.seek(0, 2) # seek to end
files[label] = f
dedup = {}
try:
while True:
for label, f in files.items():
line = f.readline()
if not line:
continue
try:
e = json.loads(line.strip())
except:
continue
if label == 'fw':
src = e.get('src_ip', '')
if not src:
continue
if filter_ip and src != filter_ip:
continue
# Filter by peer names if specified
if peer_filter:
client_name = ip_to_name.get(src, '')
if client_name not in peer_filter:
continue
dst = e.get('dest_ip', '')
port = e.get('dest_port', '')
proto_num = e.get('ip.protocol', 0)
proto = proto_map.get(proto_num, str(proto_num))
# Dedup
key = (src, dst, port, proto_num)
windows = {1: 5, 6: 30, 17: 10}
window = windows.get(proto_num, 10)
now = time.time()
if key in dedup and (now - dedup[key]) < window:
continue
dedup[key] = now
client = ip_to_name.get(src, src)
if filter_type and not client.startswith(filter_type + '-'):
continue
dst_str = f"{dst}:{port}" if port else dst
ts = e.get('timestamp', '')[:16].replace('T', ' ')
print(f"fw|{ts}|{client}|{dst_str}|{proto}", flush=True)
elif label == 'wg':
client = e.get('client', '')
if not client:
continue
if filter_ip:
ip = ip_to_name.get(filter_ip, '')
if client != ip and client != filter_ip:
continue
if peer_filter and client not in peer_filter:
continue
if filter_type and not client.startswith(filter_type + '-'):
continue
ts = e.get('timestamp', '')[:16].replace('T', ' ')
endpoint = e.get('endpoint', '')
event = e.get('event', '')
print(f"wg|{ts}|{client}|{endpoint}|{event}", flush=True)
time.sleep(0.1)
except KeyboardInterrupt:
pass
def count(file, key):
try:
with open(file) as f:
data = json.load(f)
val = data.get(key, [])
print(len(val) if isinstance(val, list) else 0)
except:
print(0)
def audit_fw_counts(clients_dir):
"""Return peer_name:fw_count pairs from iptables and client configs"""
import glob, subprocess
# Get iptables output once
try:
result = subprocess.run(
['iptables', '-L', 'FORWARD', '-n'],
capture_output=True, text=True
)
fw_output = result.stdout
except:
fw_output = ""
# Build ip->name and count rules
for conf in glob.glob(f"{clients_dir}/*.conf"):
name = os.path.basename(conf).replace('.conf', '')
try:
with open(conf) as f:
for line in f:
if line.startswith('Address'):
ip = line.split('=')[1].strip().split('/')[0]
count = fw_output.count(ip)
print(f"{name}:{count}")
break
except:
pass
def peer_group_map(groups_dir):
"""Return peer:group pairs for all groups"""
import glob
try:
for group_file in glob.glob(f"{groups_dir}/*.group"):
try:
with open(group_file) as f:
g = json.load(f)
name = g.get('name', '')
for peer in g.get('peers', []):
if peer:
print(f"{peer}:{name}")
except:
pass
except:
pass
def peer_groups(groups_dir, peer_name):
"""Find all groups containing a peer"""
import glob
try:
for group_file in glob.glob(f"{groups_dir}/*.group"):
try:
with open(group_file) as f:
g = json.load(f)
if peer_name in g.get('peers', []):
print(g.get('name', ''))
except:
pass
except:
pass
def peer_data(clients_dir, meta_dir, events_log):
import glob
meta = {}
for f in glob.glob(f"{meta_dir}/*.meta"):
name = os.path.basename(f).replace('.meta', '')
try:
with open(f) as mf:
meta[name] = json.load(mf)
except:
meta[name] = {}
last_events = {}
try:
with open(events_log) as f:
for line in f:
try:
e = json.loads(line.strip())
client = e.get('client', '')
if client:
last_events[client] = e
except:
pass
except:
pass
for conf in sorted(glob.glob(f"{clients_dir}/*.conf")):
name = os.path.basename(conf).replace('.conf', '')
ip = ''
try:
with open(conf) as f:
for line in f:
if line.startswith('Address'):
ip = line.split('=')[1].strip().split('/')[0]
break
except:
pass
m = meta.get(name, {})
rule = m.get('rule', '')
subtype = m.get('subtype', '')
last_event = last_events.get(name, {})
last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
last_evt = last_event.get('event', '') # fixed: was last_event
print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}")
def iso_to_ts(iso_str):
"""Convert ISO timestamp to unix timestamp"""
try:
from datetime import datetime, timezone
dt = datetime.fromisoformat(iso_str)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
print(int(dt.timestamp()))
except:
print(0)
def rule_list_data(rules_dir, meta_dir):
"""Return all rule data for list display in one call"""
import glob
# Count peers per rule from meta files
rule_peer_counts = {}
for f in glob.glob(f"{meta_dir}/*.meta"):
try:
with open(f) as mf:
meta = json.load(mf)
rule = meta.get('rule', '')
if rule:
rule_peer_counts[rule] = rule_peer_counts.get(rule, 0) + 1
except:
pass
# Read each rule file
for rule_file in sorted(glob.glob(f"{rules_dir}/*.rule")):
try:
with open(rule_file) as f:
r = json.load(f)
name = r.get('name', '')
desc = r.get('desc', '')
n_allows = len(r.get('allow_ips', [])) + len(r.get('allow_ports', []))
n_blocks = len(r.get('block_ips', [])) + len(r.get('block_ports', []))
peer_count = rule_peer_counts.get(name, 0)
print(f"{name}|{desc}|{n_allows}|{n_blocks}|{peer_count}")
except:
pass
def group_list_data(groups_dir, blocks_dir):
"""Return group summary data in one call"""
import glob
# Get all block files
blocked_peers = set()
for f in glob.glob(f"{blocks_dir}/*.block"):
name = os.path.basename(f).replace('.block', '')
blocked_peers.add(name)
for group_file in sorted(glob.glob(f"{groups_dir}/*.group")):
try:
with open(group_file) as f:
g = json.load(f)
name = g.get('name', '')
desc = g.get('desc', '')
peers = [p for p in g.get('peers', []) if p]
total = len(peers)
blocked = sum(1 for p in peers if p in blocked_peers)
print(f"{name}|{desc}|{total}|{blocked}")
except:
pass
def fmt_datetime(iso_str, fmt):
"""Format ISO timestamp with given strftime format"""
try:
from datetime import datetime
dt = datetime.fromisoformat(iso_str)
print(dt.strftime(fmt))
except:
print(iso_str)
def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips, block_ports):
rule = {
'name': name,
'desc': desc,
'dns_redirect': dns_redirect == 'true',
'allow_ips': [x for x in allow_ips.split(',') if x] if allow_ips else [],
'block_ips': [x for x in block_ips.split(',') if x] if block_ips else [],
'block_ports': [x for x in block_ports.split(',') if x] if block_ports else [],
'allow_ports': []
}
with open(file, 'w') as f:
json.dump(rule, f, indent=2)
def cleanup_config(config_file):
"""Normalize blank lines in WireGuard config"""
import re
try:
with open(config_file) as f:
config = f.read()
config = re.sub(r'\n{3,}', '\n\n', config)
config = config.rstrip('\n') + '\n'
with open(config_file, 'w') as f:
f.write(config)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def remove_peer_block(config_file, name):
"""Remove a peer block from WireGuard config by name"""
import re
try:
with open(config_file) as f:
config = f.read()
pattern = r'\n\[Peer\]\n# ' + re.escape(name) + r'\n[^\n]+\n[^\n]+\n'
result = re.sub(pattern, '\n', config)
with open(config_file, 'w') as f:
f.write(result)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def create_group(file, name, desc):
"""Create a new group JSON file"""
try:
group = {'name': name, 'desc': desc, 'peers': []}
with open(file, 'w') as f:
json.dump(group, f, indent=2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def parse_event(line):
"""Parse a single JSON event line"""
try:
e = json.loads(line)
print(f"{e.get('timestamp','')}|{e.get('client','')}|{e.get('endpoint','')}|{e.get('event','')}")
except:
pass
def parse_fw_event(line):
"""Parse a single fw_events.log JSON line"""
try:
e = json.loads(line)
ts = e.get('timestamp', '')
src = e.get('src_ip', '')
dst = e.get('dest_ip', '')
port = e.get('dest_port', '')
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
proto_num = e.get('ip.protocol', 0)
proto = proto_map.get(proto_num, str(proto_num))
print(f"{ts}|{src}|{dst}|{port}|{proto}")
except:
pass
def peer_transfer(wg_interface):
"""Get transfer bytes per peer from wg show"""
import subprocess
try:
result = subprocess.run(
['wg', 'show', wg_interface, 'transfer'],
capture_output=True, text=True
)
for line in result.stdout.strip().split('\n'):
if not line:
continue
parts = line.split('\t')
if len(parts) == 3:
pubkey, rx, tx = parts
total = int(rx) + int(tx)
if total == 0:
level = 'none'
elif total < 1_000_000:
level = 'low'
elif total < 10_000_000:
level = 'medium'
elif total < 100_000_000:
level = 'high'
else:
level = 'very high'
print(f"{pubkey}|{rx}|{tx}|{level}")
except:
pass
commands = {
'get': lambda args: get(args[0], args[1]),
'set': lambda args: set_key(args[0], args[1], args[2]),
'delete': lambda args: delete_key(args[0], args[1]),
'append': lambda args: append(args[0], args[1], args[2]),
'remove': lambda args: remove_value(args[0], args[1], args[2]),
'cat': lambda args: cat(args[0]),
'has_key': lambda args: has_key(args[0], args[1]),
'filter_values': lambda args: filter_values(args[0], args[1], args[2]),
'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]),
'events_for': lambda args: events_for(args[0], args[1], args[2]),
'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]),
'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]),
'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]),
'format_wg_event': lambda args: format_wg_event(sys.stdin.read()),
'remove_events': lambda args: remove_events(args[0], args[1]),
'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4], args[5]),
'count': lambda args: count(args[0], args[1]),
'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
'peer_group_map': lambda args: peer_group_map(args[0]),
'peer_groups': lambda args: peer_groups(args[0], args[1]),
'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
'iso_to_ts': lambda args: iso_to_ts(args[0]),
'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
'group_list_data': lambda args: group_list_data(args[0], args[1]),
'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]),
'create_rule': lambda args: create_rule(args[0], args[1], args[2], args[3], args[4], args[5], args[6]),
'cleanup_config': lambda args: cleanup_config(args[0]),
'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
'create_group': lambda args: create_group(args[0], args[1], args[2]),
'parse_event': lambda args: parse_event(args[0]),
'parse_fw_event': lambda args: parse_fw_event(args[0]),
'peer_transfer': lambda args: peer_transfer(args[0]),
}
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: json_helper.py <command> <file> [key] [value]", file=sys.stderr)
sys.exit(1)
cmd = sys.argv[1]
args = sys.argv[2:]
if cmd not in commands:
print(f"Unknown command: {cmd}", file=sys.stderr)
sys.exit(1)
commands[cmd](args)

View file

@ -1,109 +0,0 @@
#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function config::on_load() {
config::validate
}
# ============================================
# Server
# ============================================
WG_INTERFACE="wg0"
WG_CONFIG="$(ctx::wg)/${WG_INTERFACE}.conf"
WG_SERVER_PUBLIC_KEY_FILE="$(ctx::wg)/server_public.key"
WG_SERVER_PRIVATE_KEY_FILE="$(ctx::wg)/server_private.key"
WG_ENDPOINT="wg.krilio.net:51820"
WG_DNS="10.0.0.103"
WG_LISTEN_PORT="51820"
WG_SUBNET="10.1.0.0/16"
# ============================================
# Device Type → Subnet Mapping
# ============================================
declare -gA DEVICE_SUBNETS=(
[desktop]="10.1.1"
[laptop]="10.1.2"
[phone]="10.1.3"
[tablet]="10.1.4"
[guest]="10.1.100"
)
# ============================================
# Device Type → Default AllowedIPs
# ============================================
declare -gA DEVICE_ALLOWED_IPS=(
[desktop]="0.0.0.0/0"
[laptop]="0.0.0.0/0"
[phone]="0.0.0.0/0"
[tablet]="0.0.0.0/0"
[guest]="0.0.0.0/0"
)
# ============================================
# Accessors
# ============================================
function config::interface() { echo "$WG_INTERFACE"; }
function config::config_file() { echo "$WG_CONFIG"; }
function config::endpoint() { echo "$WG_ENDPOINT"; }
function config::dns() { echo "$WG_DNS"; }
function config::listen_port() { echo "$WG_LISTEN_PORT"; }
function config::subnet() { echo "$WG_SUBNET"; }
function config::server_public_key() {
cat "$WG_SERVER_PUBLIC_KEY_FILE"
}
function config::device_types() {
local types
{ set +u; types="${!DEVICE_SUBNETS[@]}"; set -u; }
echo "$types"
}
function config::is_valid_type() {
local type="$1"
local subnet
subnet=$(config::subnet_for "$type")
[[ -n "$subnet" ]]
}
function config::subnet_for() {
local type="$1"
local result
{ set +u; result="${DEVICE_SUBNETS[$type]:-}"; set -u; }
echo "$result"
}
function config::allowed_ips_for() {
local type="$1"
local result
{ set +u; result="${DEVICE_ALLOWED_IPS[$type]:-0.0.0.0/0}"; set -u; }
echo "$result"
}
# ============================================
# Validation
# ============================================
function config::validate() {
if [[ ! -f "$WG_SERVER_PUBLIC_KEY_FILE" ]]; then
log::error "Server public key not found: ${WG_SERVER_PUBLIC_KEY_FILE}"
exit 1
fi
if [[ ! -f "$WG_SERVER_PRIVATE_KEY_FILE" ]]; then
log::error "Server private key not found: ${WG_SERVER_PRIVATE_KEY_FILE}"
exit 1
fi
if [[ ! -f "$WG_CONFIG" ]]; then
log::error "WireGuard config not found: ${WG_CONFIG}"
exit 1
fi
}

View file

@ -1,326 +0,0 @@
#!/usr/bin/env bash
# ============================================
# Core
# ============================================
LOG_LEVEL=${LOG_LEVEL:-INFO}
# ============================================
# Internal
# ============================================
function internal::get_log_priority() {
case "$1" in
DEBUG) echo 0 ;;
INFO) echo 1 ;;
SUCCESS) echo 1 ;; # FIX: SUCCESS is informational, not above ERROR
WARN) echo 2 ;;
ERROR) echo 3 ;;
*) echo 1 ;;
esac
}
function internal::log() {
local level="$1"
shift
local current_priority
local message_priority
current_priority=$(internal::get_log_priority "$LOG_LEVEL")
message_priority=$(internal::get_log_priority "$level")
if (( message_priority < current_priority )); then
return 0
fi
local color
case "$level" in
DEBUG) color="\033[0;36m" ;; # FIX: actual cyan (was white \033[0;37m)
INFO) color="\033[1;34m" ;; # blue
WARN) color="\033[1;33m" ;; # yellow
ERROR) color="\033[1;31m" ;; # red
SUCCESS) color="\033[1;32m" ;; # green
esac
echo -e "${color}=> ${level}:\033[0m $*"
}
function internal::icon() {
local context="$1"
local action="$2"
case "$context:$action" in
docker:log) echo "🐳 " ;;
docker:start) echo "🟢 🐳 " ;;
docker:stop) echo "🔴 🐳 " ;;
docker:success) echo "✅ 🐳 " ;;
docker:warning) echo "⚠️ 🐳 " ;;
docker:error) echo "❌ 🐳 " ;;
docker:logs) echo "📜 🐳 " ;;
docker:list) echo "🔍 🐳 " ;;
docker:build) echo "📦 🐳 " ;;
build:log) echo "🏗️ " ;;
build:start) echo "🟢 🏗️ " ;;
build:stop) echo "🔴 🏗️ " ;;
build:success) echo "✅ 🏗️ " ;;
build:warning) echo "⚠️ 🏗️ " ;;
build:error) echo "❌ 🏗️ " ;;
network:log) echo "🌐 " ;;
network:setup) echo "⚙️ 🌐 " ;;
network:stop) echo "🔴 🌐 " ;;
network:success) echo "✅ 🌐 " ;;
network:warning) echo "⚠️ 🌐 " ;;
network:error) echo "❌ 🌐 " ;;
auth:log) echo "🔑 " ;;
auth:setup) echo "⚙️ 🔑 " ;;
auth:login) echo "🔐 🔑 " ;;
auth:success) echo "✅ 🔑 " ;;
auth:warning) echo "⚠️ 🔑 " ;;
auth:error) echo "❌ 🔑 " ;;
env:log) echo "⚙️ " ;;
env:load) echo "📥 ⚙️ " ;;
env:success) echo "✅ ⚙️ " ;;
env:warning) echo "⚠️ ⚙️ " ;;
env:error) echo "❌ ⚙️ " ;;
fs:log) echo "📁 " ;;
fs:read) echo "📥 📁 " ;;
fs:write) echo "📤 📁 " ;;
fs:success) echo "✅ 📁 " ;;
fs:warning) echo "⚠️ 📁 " ;;
fs:error) echo "❌ 📁 " ;;
db:log) echo "🗄️ " ;;
db:start) echo "🟢 🗄️ " ;;
db:migrate) echo "📜 🗄️ " ;;
db:success) echo "✅ 🗄️ " ;;
db:warning) echo "⚠️ 🗄️ " ;;
db:error) echo "❌ 🗄️ " ;;
# ADDED: WireGuard context
wg:log) echo "🔒 " ;;
wg:start) echo "🟢 🔒 " ;;
wg:stop) echo "🔴 🔒 " ;;
wg:add) echo " 🔒 " ;;
wg:remove) echo " 🔒 " ;;
wg:block) echo "🚫 🔒 " ;;
wg:unblock) echo "✅ 🔒 " ;;
wg:key) echo "🔑 🔒 " ;;
wg:success) echo "✅ 🔒 " ;;
wg:warning) echo "⚠️ 🔒 " ;;
wg:error) echo "❌ 🔒 " ;;
wg:list) echo "🔍 🔒 " ;;
wg:qr) echo "📱 🔒 " ;;
wg:preset) echo "📋 🔒 " ;;
log:info) echo "🔹 " ;;
log:warn) echo "⚠️ " ;;
log:error) echo "❌ " ;;
log:success) echo "✅ " ;;
log:debug) echo "🔍 " ;; # FIX: was missing, caused fallthrough
*) echo "🔹" ;;
esac
}
function internal::get_context_icon() {
case "$1" in
docker) echo "🐳" ;;
build) echo "🏗️" ;;
network) echo "🌐" ;;
auth) echo "🔑" ;;
env) echo "⚙️" ;;
fs) echo "📁" ;;
db) echo "🗄️" ;;
wg) echo "🔒" ;; # ADDED: missing from original
log) echo "🔹" ;; # ADDED: missing from original
*) echo "🔹" ;;
esac
}
# ============================================
# Loggers
# ============================================
function internal::log::info() { internal::log INFO "$*"; }
function internal::log::warn() { internal::log WARN "$*"; }
function internal::log::error() { internal::log ERROR "$*"; }
function internal::log::success() { internal::log SUCCESS "$*"; }
function internal::log::debug() { internal::log DEBUG "$*"; }
# ============================================
# Context Loggers
# ============================================
function log::context() {
local context="$1"
local action="$2"
shift 2
internal::log::info "$(internal::icon "$context" "$action") $*"
}
function log::warn_context() {
local context="$1"
local action="$2"
shift 2
internal::log::warn "$(internal::icon "$context" "$action") $*"
}
function log::error_context() {
local context="$1"
local action="$2"
shift 2
internal::log::error "$(internal::icon "$context" "$action") $*"
}
function log::success_context() {
local context="$1"
local action="$2"
shift 2
internal::log::success "$(internal::icon "$context" "$action") $*"
}
function log::debug_context() {
local context="$1"
local action="$2"
shift 2
internal::log::debug "$(internal::icon "$context" "$action") $*"
}
# ============================================
# Logger Helpers
# ============================================
function log::info() { log::context log info "$@"; }
function log::warn() { log::warn_context log warn "$@"; }
function log::error() { log::error_context log error "$@"; }
function log::success() { log::success_context log success "$@"; }
function log::debug() { log::debug_context log debug "$@"; } # FIX: was calling warn_context
# ADDED: section separator for visual grouping in output
function log::section() {
local label="$1"
local width=48
local line
line=$(printf '─%.0s' $(seq 1 $width))
echo -e "\n\033[1;34m${line}\033[0m"
echo -e "\033[1;34m $label\033[0m"
echo -e "\033[1;34m${line}\033[0m"
}
function log::docker() { log::context docker log "$@"; }
function log::docker_start() { log::context docker start "$@"; }
function log::docker_stop() { log::context docker stop "$@"; }
function log::docker_success() { log::success_context docker success "$@"; } # FIX: was using context not success_context
function log::docker_logs() { log::context docker logs "$@"; }
function log::docker_list() { log::context docker list "$@"; }
function log::docker_build() { log::context docker build "$@"; }
function log::docker_warning() { log::warn_context docker warning "$@"; }
function log::docker_error() { log::error_context docker error "$@"; }
function log::build() { log::context build log "$@"; }
function log::build_start() { log::context build start "$@"; }
function log::build_stop() { log::context build stop "$@"; }
function log::build_success() { log::success_context build success "$@"; }
function log::build_warning() { log::warn_context build warning "$@"; }
function log::build_error() { log::error_context build error "$@"; }
function log::network() { log::context network log "$@"; }
function log::network_setup() { log::context network setup "$@"; }
function log::network_stop() { log::context network stop "$@"; }
function log::network_success() { log::success_context network success "$@"; }
function log::network_warning() { log::warn_context network warning "$@"; }
function log::network_error() { log::error_context network error "$@"; }
function log::auth() { log::context auth log "$@"; }
function log::auth_setup() { log::context auth setup "$@"; }
function log::auth_login() { log::context auth login "$@"; }
function log::auth_success() { log::success_context auth success "$@"; }
function log::auth_warning() { log::warn_context auth warning "$@"; }
function log::auth_error() { log::error_context auth error "$@"; }
function log::env() { log::context env log "$@"; }
function log::env_load() { log::context env load "$@"; }
function log::env_success() { log::success_context env success "$@"; }
function log::env_warning() { log::warn_context env warning "$@"; }
function log::env_error() { log::error_context env error "$@"; }
function log::fs() { log::context fs log "$@"; }
function log::fs_read() { log::context fs read "$@"; }
function log::fs_write() { log::context fs write "$@"; }
function log::fs_success() { log::success_context fs success "$@"; }
function log::fs_warning() { log::warn_context fs warning "$@"; }
function log::fs_error() { log::error_context fs error "$@"; }
function log::db() { log::context db log "$@"; }
function log::db_start() { log::context db start "$@"; }
function log::db_migrate() { log::context db migrate "$@"; }
function log::db_success() { log::success_context db success "$@"; }
function log::db_warning() { log::warn_context db warning "$@"; }
function log::db_error() { log::error_context db error "$@"; }
# ADDED: WireGuard context helpers
function log::wg() { log::context wg log "$@"; }
function log::wg_start() { log::context wg start "$@"; }
function log::wg_stop() { log::context wg stop "$@"; }
function log::wg_add() { log::context wg add "$@"; }
function log::wg_remove() { log::context wg remove "$@"; }
function log::wg_block() { log::context wg block "$@"; }
function log::wg_unblock() { log::context wg unblock "$@"; }
function log::wg_key() { log::context wg key "$@"; }
function log::wg_list() { log::context wg list "$@"; }
function log::wg_qr() { log::context wg qr "$@"; }
function log::wg_preset() { log::context wg preset "$@"; }
function log::wg_success() { log::success_context wg success "$@"; }
function log::wg_warning() { log::warn_context wg warning "$@"; }
function log::wg_error() { log::error_context wg error "$@"; }
function log::run_step() {
local context="$1"
local mode="strict"
local description
shift
if [[ "$1" == "soft" || "$1" == "strict" || "$1" == "info" ]]; then
mode="$1"
shift
fi
description="$1"
shift
local icon
icon=$(internal::get_context_icon "$context")
if [[ "$mode" == "info" ]]; then
internal::log::info "$icon $description"
else
internal::log::info "🔄 $icon $description"
fi
"$@"
local status=$?
if [[ $status -eq 0 ]]; then
if [[ "$mode" == "info" ]]; then
return 0
fi
internal::log::success "$icon $description"
return 0
fi
if [[ "$mode" == "soft" || "$mode" == "info" ]]; then
internal::log::warn "⚠️ $icon $description → skipped"
return 0
fi
internal::log::error "$icon $description → failed"
return $status
}

View file

@ -1,192 +0,0 @@
#!/usr/bin/env bash
# ============================================
# Client Config
# ============================================
function peers::create_client_config() {
local name="$1"
local type="$2"
local ip="$3"
local allowed_ips="${4:-$(config::allowed_ips_for "$type")}"
local conf
conf="$(ctx::clients)/${name}.conf"
if [[ -f "$conf" ]]; then
log::wg_warning "Client config already exists: ${name}"
return 1
fi
local private_key
private_key=$(keys::private "$name")
local server_public_key
server_public_key=$(config::server_public_key)
cat > "$conf" <<EOF
[Interface]
PrivateKey = ${private_key}
Address = ${ip}/32
DNS = $(config::dns)
[Peer]
PublicKey = ${server_public_key}
Endpoint = $(config::endpoint)
AllowedIPs = ${allowed_ips}
PersistentKeepalive = 25
EOF
chmod 600 "$conf"
log::wg_add "Created client config: ${name} (${ip})"
}
function peers::remove_client_config() {
local name="$1"
local conf
conf="$(ctx::clients)/${name}.conf"
if [[ ! -f "$conf" ]]; then
log::wg_warning "Client config not found: ${name}"
return 1
fi
rm -f "$conf"
log::wg_remove "Removed client config: ${name}"
}
# ============================================
# Server Config (wg0.conf) Peer Management
# ============================================
function peers::cleanup_config() {
local config
config=$(config::config_file)
python3 -c "
import re
config = open('${config}').read()
# Normalize multiple blank lines to single blank line
config = re.sub(r'\n{3,}', '\n\n', config)
# Ensure file ends with single newline
config = config.rstrip('\n') + '\n'
open('${config}', 'w').write(config)
"
}
function peers::add_to_server() {
local name="$1"
local public_key="$2"
local ip="$3"
local config
config=$(config::config_file)
cat >> "$config" <<EOF
[Peer]
# ${name}
PublicKey = ${public_key}
AllowedIPs = ${ip}/32
EOF
log::wg_add "Added peer to server config: ${name}"
}
function peers::remove_block() {
local name="$1"
local config
config=$(config::config_file)
python3 -c "
import re
config = open('${config}').read()
pattern = r'\n\[Peer\]\n# ${name}\n[^\n]+\n[^\n]+\n'
result = re.sub(pattern, '\n', config)
open('${config}', 'w').write(result)
"
}
function peers::remove_from_server() {
local name="$1"
peers::remove_block "$name"
peers::cleanup_config
log::wg_remove "Removed peer from server config: ${name}"
}
# ============================================
# Listing
# ============================================
function peers::list() {
local dir
dir="$(ctx::clients)"
if [[ -z "$(ls -A "$dir"/*.conf 2>/dev/null)" ]]; then
log::wg_list "No clients configured"
return 0
fi
for conf in "${dir}"/*.conf; do
local client_name
client_name=$(basename "$conf" .conf)
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local public_key
public_key=$(keys::public "$client_name" 2>/dev/null || echo "unknown")
# Determine type from IP
local type="unknown"
for t in $(config::device_types); do
local subnet
subnet=$(config::subnet_for "$t")
if string::starts_with "$ip" "$subnet"; then
type="$t"
break
fi
done
printf " %-30s %-15s %-10s %s\n" \
"$client_name" "$ip" "$type" "$public_key"
done
}
function peers::list_by_type() {
local filter_type="$1"
local dir
dir="$(ctx::clients)"
for conf in "${dir}"/*.conf; do
local client_name
client_name=$(basename "$conf" .conf)
local ip
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
local subnet
subnet=$(config::subnet_for "$filter_type")
if string::starts_with "$ip" "$subnet"; then
printf " %-30s %-15s\n" "$client_name" "$ip"
fi
done
}
function peers::exists_in_server() {
local name="$1"
grep -q "^# ${name}$" "$(config::config_file)"
}
# ============================================
# Live Reload
# ============================================
function peers::reload() {
wg syncconf "$(config::interface)" <(wg-quick strip "$(config::interface)")
log::wg_success "WireGuard config reloaded"
}