cleanup structure,bak files | change peers header to match fw in rule inspect
This commit is contained in:
parent
7323bf20f1
commit
4ac25e283d
7 changed files with 8 additions and 2047 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue