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
|
# 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() {
|
function cmd::rule::show() {
|
||||||
local name="" show_peers=true show_resolved=false
|
local name="" show_peers=true show_resolved=false
|
||||||
|
|
||||||
|
|
@ -589,16 +380,21 @@ function cmd::rule::show() {
|
||||||
<<< "$res_allow_ports"$'\n'"$res_allow_ips"
|
<<< "$res_allow_ports"$'\n'"$res_allow_ips"
|
||||||
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \
|
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \
|
||||||
<<< "$res_block_ips"$'\n'"$res_block_ports"
|
<<< "$res_block_ips"$'\n'"$res_block_ports"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Peers ─────────────────────────────────────
|
# ── Peers ─────────────────────────────────────
|
||||||
cmd::rule::_show_section "Peers"
|
|
||||||
local peer_list=()
|
local peer_list=()
|
||||||
mapfile -t peer_list < <(peers::with_rule "$name")
|
mapfile -t peer_list < <(peers::with_rule "$name")
|
||||||
|
|
||||||
local peer_count=${#peer_list[@]}
|
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
|
if $show_peers && [[ $peer_count -gt 0 ]]; then
|
||||||
printf "\n"
|
|
||||||
for peer_name in "${peer_list[@]}"; do
|
for peer_name in "${peer_list[@]}"; do
|
||||||
local ip
|
local ip
|
||||||
ip=$(peers::get_ip "$peer_name")
|
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