Compare commits
No commits in common. "master" and "refactor/config-restructure" have entirely different histories.
master
...
refactor/c
56 changed files with 227 additions and 3399 deletions
|
|
@ -6,8 +6,6 @@
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::activity::on_load() {
|
function cmd::activity::on_load() {
|
||||||
command::mixin json_output
|
|
||||||
|
|
||||||
load_module net
|
load_module net
|
||||||
|
|
||||||
flag::register --peer
|
flag::register --peer
|
||||||
|
|
@ -15,14 +13,9 @@ function cmd::activity::on_load() {
|
||||||
flag::register --ip
|
flag::register --ip
|
||||||
flag::register --hours
|
flag::register --hours
|
||||||
flag::register --type
|
flag::register --type
|
||||||
flag::register --accept
|
flag::register --dropped
|
||||||
flag::register --drop
|
|
||||||
flag::register --external
|
command::mixin json_output
|
||||||
flag::register --ports
|
|
||||||
flag::register --exclude-service
|
|
||||||
flag::register --include-service
|
|
||||||
|
|
||||||
flag::exclusive --accept --drop
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -42,12 +35,11 @@ Options:
|
||||||
--ip <ip> Filter by destination IP
|
--ip <ip> Filter by destination IP
|
||||||
--hours <n> Time window in hours (default: 24, 0 = all time)
|
--hours <n> Time window in hours (default: 24, 0 = all time)
|
||||||
--type <type> Filter by device type (combined with --peer)
|
--type <type> Filter by device type (combined with --peer)
|
||||||
--accept Show only accepted traffic (from conntrack)
|
--dropped Show only peers with at least one drop
|
||||||
--drop Show only firewall drops
|
|
||||||
--external Show only external traffic (full tunnel peers)
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl activity
|
wgctl activity
|
||||||
|
wgctl activity --dropped
|
||||||
wgctl activity --peer phone-nuno
|
wgctl activity --peer phone-nuno
|
||||||
wgctl activity --service truenas
|
wgctl activity --service truenas
|
||||||
wgctl activity --hours 0
|
wgctl activity --hours 0
|
||||||
|
|
@ -61,24 +53,17 @@ EOF
|
||||||
|
|
||||||
function cmd::activity::run() {
|
function cmd::activity::run() {
|
||||||
local filter_peer="" filter_service="" filter_ip="" filter_type=""
|
local filter_peer="" filter_service="" filter_ip="" filter_type=""
|
||||||
local hours=24
|
local hours=24 dropped_only=false
|
||||||
local accept_only=false drop_only=false external_only=false show_ports=false
|
|
||||||
local -a exclude_services=() include_services=()
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--peer) filter_peer="$2"; shift 2 ;;
|
--peer) filter_peer="$2"; shift 2 ;;
|
||||||
--service) filter_service="$2"; shift 2 ;;
|
--service) filter_service="$2"; shift 2 ;;
|
||||||
--ip) filter_ip="$2"; shift 2 ;;
|
--ip) filter_ip="$2"; shift 2 ;;
|
||||||
--type) filter_type="$2"; shift 2 ;;
|
--type) filter_type="$2"; shift 2 ;;
|
||||||
--hours) hours="$2"; shift 2 ;;
|
--hours) hours="$2"; shift 2 ;;
|
||||||
--accept) accept_only=true; shift ;;
|
--dropped) dropped_only=true; shift ;;
|
||||||
--drop) drop_only=true; shift ;;
|
--help) cmd::activity::help; return ;;
|
||||||
--external) external_only=true; shift ;;
|
|
||||||
--ports) show_ports=true; shift ;;
|
|
||||||
--exclude-service) exclude_services+=("$2"); shift 2 ;;
|
|
||||||
--include-service) include_services+=("$2"); shift 2 ;;
|
|
||||||
--help) cmd::activity::help; return ;;
|
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
cmd::activity::help
|
cmd::activity::help
|
||||||
|
|
@ -92,82 +77,42 @@ function cmd::activity::run() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Resolve peer name if type provided
|
||||||
if [[ -n "$filter_peer" && -n "$filter_type" ]]; then
|
if [[ -n "$filter_peer" && -n "$filter_type" ]]; then
|
||||||
filter_peer=$(peers::resolve_and_require "$filter_peer" "$filter_type") || return 1
|
filter_peer=$(peers::resolve_and_require "$filter_peer" "$filter_type") || return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Resolve --service to IP
|
||||||
local service_ip=""
|
local service_ip=""
|
||||||
if [[ -n "$filter_service" ]]; then
|
if [[ -n "$filter_service" ]]; then
|
||||||
service_ip=$(net::resolve "$filter_service" 2>/dev/null | head -1 | cut -d: -f1) || true
|
service_ip=$(net::resolve "$filter_service" 2>/dev/null | head -1 | cut -d: -f1) || true
|
||||||
[[ -z "$service_ip" ]] && log::error "Service not found: ${filter_service}" && return 1
|
if [[ -z "$service_ip" ]]; then
|
||||||
|
log::error "Service not found: ${filter_service}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
[[ -n "$filter_ip" ]] && service_ip="$filter_ip"
|
[[ -n "$filter_ip" ]] && service_ip="$filter_ip"
|
||||||
|
|
||||||
# Build final exclusion list — remove any --include-service entries
|
# Fetch aggregated data
|
||||||
local -a final_excludes=()
|
local data
|
||||||
for svc in "${exclude_services[@]:-}"; do
|
data=$(json::activity_aggregate \
|
||||||
local included=false
|
"$(ctx::fw_events_log)" \
|
||||||
for inc in "${include_services[@]:-}"; do
|
"$(ctx::events_log)" \
|
||||||
[[ "$svc" == "$inc" ]] && included=true && break
|
"$(config::interface)" \
|
||||||
done
|
"$(ctx::net)" \
|
||||||
$included || final_excludes+=("$svc")
|
"$(ctx::clients)" \
|
||||||
done
|
"$(ctx::meta)" \
|
||||||
|
"$hours" \
|
||||||
# Build exclude string for Python (space-separated)
|
"$filter_peer" \
|
||||||
local exclude_str=""
|
"$service_ip" 2>/dev/null)
|
||||||
[[ ${#final_excludes[@]} -gt 0 ]] && \
|
|
||||||
exclude_str=$(IFS=' '; echo "${final_excludes[*]}")
|
|
||||||
|
|
||||||
# ── Fetch data ──
|
if [[ -z "$data" ]]; then
|
||||||
local data=""
|
log::wg_warning "No activity data found"
|
||||||
if ! $accept_only; then
|
return 0
|
||||||
data=$(json::activity_aggregate \
|
|
||||||
"$(ctx::fw_events_log)" "$(ctx::events_log)" \
|
|
||||||
"$(config::interface)" "$(ctx::net)" \
|
|
||||||
"$(ctx::clients)" "$(ctx::meta)" \
|
|
||||||
"$hours" "$filter_peer" "$service_ip" "$exclude_str" 2>/dev/null)
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local accept_data=""
|
# Measure column widths
|
||||||
if ! $drop_only; then
|
local w_peer=16 w_drops=1
|
||||||
local since_arg="" ext_flag="0"
|
|
||||||
[[ "$hours" -gt 0 ]] && since_arg="${hours}h"
|
|
||||||
$external_only && ext_flag="1"
|
|
||||||
[[ -f "$(ctx::accept_events_log)" ]] && \
|
|
||||||
accept_data=$(json::accept_aggregate \
|
|
||||||
"$(ctx::accept_events_log)" "$(ctx::net)" "$(ctx::clients)" \
|
|
||||||
"$since_arg" "$filter_peer" "$ext_flag" "$exclude_str" 2>/dev/null)
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ -z "$data" && -z "$accept_data" ]] && \
|
|
||||||
log::wg_warning "No activity data found" && return 0
|
|
||||||
|
|
||||||
# ── Build accept maps ──
|
|
||||||
declare -gA _ACCEPT_PEER=()
|
|
||||||
declare -gA _ACCEPT_DEST_KEYS=()
|
|
||||||
declare -gA _ACCEPT_DEST=()
|
|
||||||
|
|
||||||
while IFS='|' read -r type rest; do
|
|
||||||
[[ -z "$type" ]] && continue
|
|
||||||
case "$type" in
|
|
||||||
peer)
|
|
||||||
local a_name a_bi a_bo a_pi a_po a_conns
|
|
||||||
IFS='|' read -r a_name a_bi a_bo a_pi a_po a_conns <<< "$rest"
|
|
||||||
_ACCEPT_PEER["$a_name"]="${a_bi}|${a_bo}|${a_pi}|${a_po}|${a_conns}"
|
|
||||||
;;
|
|
||||||
dest)
|
|
||||||
local d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count
|
|
||||||
IFS='|' read -r d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count <<< "$rest"
|
|
||||||
local d_key="${d_peer}:${d_ip}:${d_port}:${d_proto}"
|
|
||||||
_ACCEPT_DEST["$d_key"]="${d_bytes_orig}|${d_bytes_reply}|${d_count}"
|
|
||||||
_ACCEPT_DEST_KEYS["$d_peer"]+="${d_key} "
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <<< "$accept_data"
|
|
||||||
|
|
||||||
# ── Measure column widths ──
|
|
||||||
local w_peer=16 w_count=1
|
|
||||||
|
|
||||||
while IFS='|' read -r type rest; do
|
while IFS='|' read -r type rest; do
|
||||||
case "$type" in
|
case "$type" in
|
||||||
peer)
|
peer)
|
||||||
|
|
@ -175,238 +120,80 @@ function cmd::activity::run() {
|
||||||
name=$(echo "$rest" | cut -d'|' -f1)
|
name=$(echo "$rest" | cut -d'|' -f1)
|
||||||
drops=$(echo "$rest" | cut -d'|' -f4)
|
drops=$(echo "$rest" | cut -d'|' -f4)
|
||||||
(( ${#name} > w_peer )) && w_peer=${#name}
|
(( ${#name} > w_peer )) && w_peer=${#name}
|
||||||
(( ${#drops} > w_count )) && w_count=${#drops}
|
(( ${#drops} > w_drops )) && w_drops=${#drops}
|
||||||
;;
|
;;
|
||||||
service)
|
service)
|
||||||
local svc_count
|
local count
|
||||||
svc_count=$(echo "$rest" | cut -d'|' -f3)
|
count=$(echo "$rest" | cut -d'|' -f3)
|
||||||
(( ${#svc_count} > w_count )) && w_count=${#svc_count}
|
(( ${#count} > w_drops )) && w_drops=${#count}
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
|
|
||||||
for a_name in "${!_ACCEPT_PEER[@]}"; do
|
|
||||||
(( ${#a_name} > w_peer )) && w_peer=${#a_name}
|
|
||||||
local a_conns_val="${_ACCEPT_PEER[$a_name]##*|}"
|
|
||||||
(( ${#a_conns_val} > w_count )) && w_count=${#a_conns_val}
|
|
||||||
done
|
|
||||||
for key in "${!_ACCEPT_DEST[@]}"; do
|
|
||||||
local d_val="${_ACCEPT_DEST[$key]}"
|
|
||||||
local d_count_val="${d_val##*|}"
|
|
||||||
(( ${#d_count_val} > w_count )) && w_count=${#d_count_val}
|
|
||||||
done
|
|
||||||
|
|
||||||
(( w_peer += 2 ))
|
(( w_peer += 2 ))
|
||||||
|
|
||||||
|
# Compute column where drop count starts on peer row:
|
||||||
|
# " " (2) + name (w_peer) + " ↓" (3) + rx (10) + " ↑" (3) + tx (10) + " " (2)
|
||||||
|
# ↓ and ↑ are multi-byte (3 bytes, 1 visible) — 2 extra bytes each
|
||||||
|
# Visible: 2 + w_peer + 2+1 + 10 + 2+1 + 10 + 2 = w_peer + 30
|
||||||
local drops_col=$(( w_peer + 30 ))
|
local drops_col=$(( w_peer + 30 ))
|
||||||
|
|
||||||
local hours_display="${hours}h"
|
local hours_display="${hours}h"
|
||||||
[[ "$hours" == "0" ]] && hours_display="all time"
|
[[ "$hours" == "0" ]] && hours_display="all time"
|
||||||
|
|
||||||
log::section "Activity Monitor (last ${hours_display})"
|
log::section "Activity Monitor (last ${hours_display})"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if display::is_table "activity"; then
|
local first_peer=true skip_peer=false
|
||||||
cmd::activity::_render_table "$data"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Accept dest inline renderer ──
|
|
||||||
_render_peer_accept_dests() {
|
|
||||||
local peer_name="$1"
|
|
||||||
local keys="${_ACCEPT_DEST_KEYS[$peer_name]:-}"
|
|
||||||
[[ -z "$keys" ]] && return 0
|
|
||||||
for d_key in $keys; do
|
|
||||||
local dest_stats="${_ACCEPT_DEST[$d_key]:-}"
|
|
||||||
[[ -z "$dest_stats" ]] && continue
|
|
||||||
local d_bytes_orig d_bytes_reply d_count
|
|
||||||
IFS='|' read -r d_bytes_orig d_bytes_reply d_count <<< "$dest_stats"
|
|
||||||
local rest_key="${d_key#${peer_name}:}"
|
|
||||||
local d_ip="${rest_key%%:*}"
|
|
||||||
local pp="${rest_key#*:}"
|
|
||||||
local d_port="${pp%%:*}"
|
|
||||||
local d_proto="${pp##*:}"
|
|
||||||
local spec="${d_ip}:${d_port}:${d_proto}"
|
|
||||||
local dest_display
|
|
||||||
local raw_suffix=""
|
|
||||||
local resolved="${_DEST_RESOLVE_CACHE[$spec]:-${d_ip}:${d_port}/${d_proto}}"
|
|
||||||
local dest_display="$resolved"
|
|
||||||
if [[ "$show_ports" == "true" && "$resolved" != "${d_ip}:"* && "$resolved" != "${d_ip} "* ]]; then
|
|
||||||
if [[ -n "$d_port" && "$d_port" != "0" ]]; then
|
|
||||||
dest_display=$(printf "%s \033[2m(%s:%s)\033[0m" "$resolved" "$d_ip" "$d_port")
|
|
||||||
else
|
|
||||||
dest_display=$(printf "%s \033[2m(%s)\033[0m" "$resolved" "$d_ip")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
ui::activity::accept_dest_row \
|
|
||||||
"$dest_display" "$d_bytes_orig" "$d_bytes_reply" \
|
|
||||||
"$d_count" "$drops_col" "$w_count"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
declare -gA _DEST_RESOLVE_CACHE=()
|
|
||||||
local -a _dest_specs=()
|
|
||||||
for _dk in "${!_ACCEPT_DEST[@]}"; do
|
|
||||||
# key format: peer:ip:port:proto — strip peer prefix
|
|
||||||
local _rest="${_dk#*:}"
|
|
||||||
local _dip="${_rest%%:*}"
|
|
||||||
local _pp="${_rest#*:}"
|
|
||||||
local _dport="${_pp%%:*}"
|
|
||||||
local _dproto="${_pp##*:}"
|
|
||||||
local _spec="${_dip}:${_dport}:${_dproto}"
|
|
||||||
# Deduplicate
|
|
||||||
local _found=false
|
|
||||||
for _s in "${_dest_specs[@]:-}"; do
|
|
||||||
[[ "$_s" == "$_spec" ]] && _found=true && break
|
|
||||||
done
|
|
||||||
$_found || _dest_specs+=("$_spec")
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#_dest_specs[@]} -gt 0 ]]; then
|
|
||||||
while IFS='|' read -r _spec _display; do
|
|
||||||
[[ -n "$_spec" ]] && _DEST_RESOLVE_CACHE["$_spec"]="$_display"
|
|
||||||
done < <(json::batch_resolve_dest "${_dest_specs[@]}" 2>/dev/null)
|
|
||||||
fi
|
|
||||||
|
|
||||||
local first_peer=true skip_peer=false current_name=""
|
|
||||||
local -a rendered_peers=()
|
|
||||||
|
|
||||||
# ── Main render loop (drop data) ──
|
|
||||||
while IFS='|' read -r record_type rest; do
|
while IFS='|' read -r record_type rest; do
|
||||||
case "$record_type" in
|
case "$record_type" in
|
||||||
peer)
|
peer)
|
||||||
local name rx tx drops
|
local name rx tx drops
|
||||||
IFS='|' read -r name rx tx drops <<< "$rest"
|
IFS='|' read -r name rx tx drops <<< "$rest"
|
||||||
|
|
||||||
# Flush previous peer's accept dests
|
|
||||||
[[ -n "$current_name" ]] && ! $drop_only && \
|
|
||||||
_render_peer_accept_dests "$current_name"
|
|
||||||
|
|
||||||
skip_peer=false
|
skip_peer=false
|
||||||
current_name="$name"
|
if $dropped_only && [[ "$drops" -eq 0 ]]; then
|
||||||
local has_accept="${_ACCEPT_PEER[$name]:-}"
|
skip_peer=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
$first_peer || echo ""
|
$first_peer || echo ""
|
||||||
first_peer=false
|
first_peer=false
|
||||||
rendered_peers+=("$name")
|
|
||||||
|
|
||||||
local rx_fmt tx_fmt
|
local rx_fmt tx_fmt
|
||||||
rx_fmt=$(fmt::bytes "$rx")
|
rx_fmt=$(fmt::bytes "$rx")
|
||||||
tx_fmt=$(fmt::bytes "$tx")
|
tx_fmt=$(fmt::bytes "$tx")
|
||||||
|
|
||||||
local name_pad rx_pad tx_pad
|
local name_pad rx_pad tx_pad
|
||||||
name_pad=$(printf "%-${w_peer}s" "$name")
|
name_pad=$(printf "%-${w_peer}s" "$name")
|
||||||
rx_pad=$(printf "%-10s" "$rx_fmt")
|
rx_pad=$(printf "%-10s" "$rx_fmt")
|
||||||
tx_pad=$(printf "%-10s" "$tx_fmt")
|
tx_pad=$(printf "%-10s" "$tx_fmt")
|
||||||
|
|
||||||
local drop_word="drops"
|
local drop_word="drops"
|
||||||
[[ "$drops" -eq 1 ]] && drop_word="drop"
|
[[ "$drops" -eq 1 ]] && drop_word="drop"
|
||||||
|
|
||||||
# Always show peer name — either full row or name-only for accept_only
|
|
||||||
if $accept_only; then
|
|
||||||
printf " \033[1m%s\033[0m\n" "$name_pad"
|
|
||||||
else
|
|
||||||
ui::activity::peer_row \
|
|
||||||
"$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word" "$w_count"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Accept summary row
|
|
||||||
if [[ -n "$has_accept" ]] && ! $drop_only; then
|
|
||||||
local a_bi a_bo a_pi a_po a_conns
|
|
||||||
IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$has_accept"
|
|
||||||
ui::activity::accept_row \
|
|
||||||
"$name_pad" \
|
|
||||||
"$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
|
|
||||||
"$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
|
|
||||||
"$a_conns" "$w_count"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
service)
|
|
||||||
local peer dest_display dst_ip dst_port proto drop_count
|
|
||||||
IFS='|' read -r peer dest_display dst_ip dst_port proto drop_count <<< "$rest"
|
|
||||||
# Build dim suffix if --ports
|
|
||||||
local svc_display="$dest_display"
|
|
||||||
if [[ "$show_ports" == "true" && -n "$dst_ip" ]]; then
|
|
||||||
if [[ -n "$dst_port" ]]; then
|
|
||||||
svc_display=$(printf "%s \033[2m(%s:%s)\033[0m" \
|
|
||||||
"$dest_display" "$dst_ip" "$dst_port")
|
|
||||||
else
|
|
||||||
svc_display=$(printf "%s \033[2m(%s)\033[0m" \
|
|
||||||
"$dest_display" "$dst_ip")
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
local svc_drop_word="drops"
|
|
||||||
[[ "$drop_count" -eq 1 ]] && svc_drop_word="drop"
|
|
||||||
$accept_only || ui::activity::service_row \
|
|
||||||
"$svc_display" "$drop_count" "$svc_drop_word" "$drops_col" "$w_count"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <<< "$data"
|
|
||||||
|
|
||||||
# Flush last peer's accept dests
|
|
||||||
[[ -n "$current_name" ]] && ! $drop_only && \
|
|
||||||
_render_peer_accept_dests "$current_name"
|
|
||||||
|
|
||||||
# ── Accept-only peers (not in drop data) ──
|
|
||||||
if ! $drop_only; then
|
|
||||||
for a_name in $(echo "${!_ACCEPT_PEER[@]}" | tr ' ' '\n' | sort); do
|
|
||||||
# Skip already rendered
|
|
||||||
local already=false
|
|
||||||
for rp in "${rendered_peers[@]:-}"; do
|
|
||||||
[[ "$rp" == "$a_name" ]] && already=true && break
|
|
||||||
done
|
|
||||||
$already && continue
|
|
||||||
|
|
||||||
$first_peer || echo ""
|
|
||||||
first_peer=false
|
|
||||||
|
|
||||||
local a_stats="${_ACCEPT_PEER[$a_name]}"
|
|
||||||
local a_bi a_bo a_pi a_po a_conns
|
|
||||||
IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$a_stats"
|
|
||||||
local name_pad
|
|
||||||
name_pad=$(printf "%-${w_peer}s" "$a_name")
|
|
||||||
|
|
||||||
# Always show peer name
|
|
||||||
printf " \033[1m%s\033[0m\n" "$name_pad"
|
|
||||||
|
|
||||||
ui::activity::accept_row \
|
|
||||||
"$name_pad" \
|
|
||||||
"$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
|
|
||||||
"$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
|
|
||||||
"$a_conns" "$w_count"
|
|
||||||
_render_peer_accept_dests "$a_name"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::activity::_render_table() {
|
|
||||||
local data="${1:-}"
|
|
||||||
[[ -z "$data" ]] && return 0
|
|
||||||
|
|
||||||
ui::activity::header_table
|
ui::activity::peer_row \
|
||||||
local skip_peer=false
|
"$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word" "$w_drops"
|
||||||
while IFS='|' read -r record_type rest; do
|
|
||||||
case "$record_type" in
|
|
||||||
peer)
|
|
||||||
local name rx tx drops
|
|
||||||
IFS='|' read -r name rx tx drops <<< "$rest"
|
|
||||||
skip_peer=false
|
|
||||||
local rx_fmt tx_fmt
|
|
||||||
rx_fmt=$(fmt::bytes "$rx")
|
|
||||||
tx_fmt=$(fmt::bytes "$tx")
|
|
||||||
ui::activity::peer_row_table "$name" "$rx_fmt" "$tx_fmt" "$drops" ""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
service)
|
service)
|
||||||
$skip_peer && continue
|
$skip_peer && continue
|
||||||
local peer dest count
|
|
||||||
IFS='|' read -r peer dest count <<< "$rest"
|
local peer dest_display drop_count
|
||||||
ui::activity::service_row_table "$dest" "$count" "drops"
|
IFS='|' read -r peer dest_display drop_count <<< "$rest"
|
||||||
|
|
||||||
|
local svc_drop_word="drops"
|
||||||
|
[[ "$drop_count" -eq 1 ]] && svc_drop_word="drop"
|
||||||
|
|
||||||
|
ui::activity::service_row \
|
||||||
|
"$dest_display" "$drop_count" "$svc_drop_word" "$drops_col" "$w_drops"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done <<< "$data"
|
done <<< "$data"
|
||||||
}
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
function cmd::activity::_output_json() {
|
function cmd::activity::_output_json() {
|
||||||
local hours="${1:-24}"
|
local hours="${1:-24}"
|
||||||
|
|
@ -456,48 +243,3 @@ function cmd::activity::_output_json() {
|
||||||
array=$(printf '%s\n' "${peers[@]:-}" | paste -sd ',' -)
|
array=$(printf '%s\n' "${peers[@]:-}" | paste -sd ',' -)
|
||||||
printf '{"peers":[%s]}' "${array:-}" | json::envelope "activity" "$count"
|
printf '{"peers":[%s]}' "${array:-}" | json::envelope "activity" "$count"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::activity::_fetch_accept_data() {
|
|
||||||
local hours="${1:-24}" filter_peer="${2:-}" external_only="${3:-false}"
|
|
||||||
|
|
||||||
[[ ! -f "$(ctx::accept_events_log)" ]] && return 0
|
|
||||||
|
|
||||||
local since_arg=""
|
|
||||||
[[ "$hours" -gt 0 ]] && since_arg="${hours}h"
|
|
||||||
|
|
||||||
local ext_flag="0"
|
|
||||||
$external_only && ext_flag="1"
|
|
||||||
|
|
||||||
json::accept_aggregate \
|
|
||||||
"$(ctx::accept_events_log)" \
|
|
||||||
"$(ctx::net)" \
|
|
||||||
"$(ctx::clients)" \
|
|
||||||
"$since_arg" \
|
|
||||||
"$filter_peer" \
|
|
||||||
2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::activity::_build_accept_maps() {
|
|
||||||
local accept_data="${1:-}"
|
|
||||||
# Outputs to stdout as bash declare statements — use eval
|
|
||||||
# Sets: _ACCEPT_PEER[name]="bytes_in|bytes_out|packets_in|packets_out|conn_count"
|
|
||||||
# _ACCEPT_DEST[name:ip:port:proto]="bytes|count"
|
|
||||||
declare -gA _ACCEPT_PEER=()
|
|
||||||
declare -gA _ACCEPT_DEST=()
|
|
||||||
|
|
||||||
while IFS='|' read -r type rest; do
|
|
||||||
[[ -z "$type" ]] && continue
|
|
||||||
case "$type" in
|
|
||||||
peer)
|
|
||||||
local name bytes_in bytes_out packets_in packets_out conn_count
|
|
||||||
IFS='|' read -r name bytes_in bytes_out packets_in packets_out conn_count <<< "$rest"
|
|
||||||
_ACCEPT_PEER["$name"]="${bytes_in}|${bytes_out}|${packets_in}|${packets_out}|${conn_count}"
|
|
||||||
;;
|
|
||||||
dest)
|
|
||||||
local peer dst_ip dst_port proto bytes count
|
|
||||||
IFS='|' read -r peer dst_ip dst_port proto bytes count <<< "$rest"
|
|
||||||
_ACCEPT_DEST["${peer}:${dst_ip}:${dst_port}:${proto}"]="${bytes}|${count}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <<< "$accept_data"
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,6 @@ function cmd::block::on_load() {
|
||||||
flag::register --subnet
|
flag::register --subnet
|
||||||
flag::register --block-name
|
flag::register --block-name
|
||||||
flag::register --service
|
flag::register --service
|
||||||
flag::register --reason
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -62,8 +61,7 @@ function cmd::block::run() {
|
||||||
local name="" identity="" type="" block_name=""
|
local name="" identity="" type="" block_name=""
|
||||||
local ips=() subnets=() ports=() services=()
|
local ips=() subnets=() ports=() services=()
|
||||||
local quiet=false force=false
|
local quiet=false force=false
|
||||||
local reason=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
|
|
@ -76,7 +74,6 @@ function cmd::block::run() {
|
||||||
--quiet) quiet=true; shift ;;
|
--quiet) quiet=true; shift ;;
|
||||||
--subnet) subnets+=("$2"); shift 2 ;;
|
--subnet) subnets+=("$2"); shift 2 ;;
|
||||||
--port) ports+=("$2"); shift 2 ;;
|
--port) ports+=("$2"); shift 2 ;;
|
||||||
--reason) reason="$2"; shift 2 ;;
|
|
||||||
--help) cmd::block::help; return ;;
|
--help) cmd::block::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
|
|
@ -85,25 +82,25 @@ function cmd::block::run() {
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# --identity: block all peers for this identity
|
# --identity: block all peers for this identity
|
||||||
if [[ -n "$identity" ]]; then
|
if [[ -n "$identity" ]]; then
|
||||||
cmd::block::_block_identity "$identity" "$quiet" \
|
cmd::block::_block_identity "$identity" "$quiet" \
|
||||||
"${ips[@]+"${ips[@]}"}" || return 1
|
"${ips[@]+"${ips[@]}"}" || return 1
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -z "$name" ]] && {
|
[[ -z "$name" ]] && {
|
||||||
log::error "Missing required flag: --name or --identity"
|
log::error "Missing required flag: --name or --identity"
|
||||||
cmd::block::help
|
cmd::block::help
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
local client_ip
|
local client_ip
|
||||||
client_ip=$(peers::get_ip "$name") || return 1
|
client_ip=$(peers::get_ip "$name") || return 1
|
||||||
|
|
||||||
# Full block if no specific targets
|
# Full block if no specific targets
|
||||||
if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \
|
if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \
|
||||||
${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
|
${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
|
||||||
|
|
@ -113,10 +110,9 @@ function cmd::block::run() {
|
||||||
fi
|
fi
|
||||||
monitor::update_endpoint_cache
|
monitor::update_endpoint_cache
|
||||||
cmd::block::_block_all "$name" "$client_ip" "$quiet"
|
cmd::block::_block_all "$name" "$client_ip" "$quiet"
|
||||||
cmd::block::_record_history "$name" "full" "manual" "$reason"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Specific rules — check if already fully blocked
|
# Specific rules — check if already fully blocked
|
||||||
if block::has_file "$name"; then
|
if block::has_file "$name"; then
|
||||||
local direct
|
local direct
|
||||||
|
|
@ -126,9 +122,9 @@ function cmd::block::run() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local changed=false
|
local changed=false
|
||||||
|
|
||||||
# Block specific IPs
|
# Block specific IPs
|
||||||
for ip in "${ips[@]}"; do
|
for ip in "${ips[@]}"; do
|
||||||
ip::require_valid "$ip"
|
ip::require_valid "$ip"
|
||||||
|
|
@ -136,7 +132,7 @@ function cmd::block::run() {
|
||||||
block::add_rule "$name" "$client_ip" "ip" "${block_name:-}" "$ip"
|
block::add_rule "$name" "$client_ip" "ip" "${block_name:-}" "$ip"
|
||||||
$quiet || log::wg_success "${ip} has been blocked for ${name}"
|
$quiet || log::wg_success "${ip} has been blocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Block specific subnets
|
# Block specific subnets
|
||||||
for subnet in "${subnets[@]}"; do
|
for subnet in "${subnets[@]}"; do
|
||||||
ip::require_valid "$subnet"
|
ip::require_valid "$subnet"
|
||||||
|
|
@ -144,7 +140,7 @@ function cmd::block::run() {
|
||||||
block::add_rule "$name" "$client_ip" "subnet" "${block_name:-}" "$subnet"
|
block::add_rule "$name" "$client_ip" "subnet" "${block_name:-}" "$subnet"
|
||||||
$quiet || log::wg_success "${subnet} has been blocked for ${name}"
|
$quiet || log::wg_success "${subnet} has been blocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Block specific ports
|
# Block specific ports
|
||||||
for entry in "${ports[@]}"; do
|
for entry in "${ports[@]}"; do
|
||||||
local b_target b_port b_proto
|
local b_target b_port b_proto
|
||||||
|
|
@ -155,7 +151,7 @@ function cmd::block::run() {
|
||||||
"$b_target" "$b_port" "${b_proto:-tcp}"
|
"$b_target" "$b_port" "${b_proto:-tcp}"
|
||||||
$quiet || log::wg_success "${client_ip}:${b_port}:${b_proto:-tcp} has been blocked for ${name}"
|
$quiet || log::wg_success "${client_ip}:${b_port}:${b_proto:-tcp} has been blocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Block services
|
# Block services
|
||||||
for svc in "${services[@]}"; do
|
for svc in "${services[@]}"; do
|
||||||
local resolved_lines=()
|
local resolved_lines=()
|
||||||
|
|
@ -164,7 +160,7 @@ function cmd::block::run() {
|
||||||
log::error "Service not found or has no ports: ${svc}"
|
log::error "Service not found or has no ports: ${svc}"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local already_blocked=true
|
local already_blocked=true
|
||||||
for resolved in "${resolved_lines[@]}"; do
|
for resolved in "${resolved_lines[@]}"; do
|
||||||
if [[ "$resolved" == *:*:* ]]; then
|
if [[ "$resolved" == *:*:* ]]; then
|
||||||
|
|
@ -177,12 +173,12 @@ function cmd::block::run() {
|
||||||
{ already_blocked=false; break; }
|
{ already_blocked=false; break; }
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if $already_blocked; then
|
if $already_blocked; then
|
||||||
$quiet || log::wg_warning "${svc} is already blocked for ${name}"
|
$quiet || log::wg_warning "${svc} is already blocked for ${name}"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for resolved in "${resolved_lines[@]}"; do
|
for resolved in "${resolved_lines[@]}"; do
|
||||||
if [[ "$resolved" == *:*:* ]]; then
|
if [[ "$resolved" == *:*:* ]]; then
|
||||||
local b_ip b_port b_proto
|
local b_ip b_port b_proto
|
||||||
|
|
@ -195,14 +191,14 @@ function cmd::block::run() {
|
||||||
block::add_rule "$name" "$client_ip" "ip" "$svc" "$resolved"
|
block::add_rule "$name" "$client_ip" "ip" "$svc" "$resolved"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
changed=true
|
changed=true
|
||||||
$quiet || log::wg_success "${svc} has been blocked for ${name}"
|
$quiet || log::wg_success "${svc} has been blocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ ${#ips[@]} -gt 0 || ${#ports[@]} -gt 0 || \
|
[[ ${#ips[@]} -gt 0 || ${#ports[@]} -gt 0 || \
|
||||||
${#subnets[@]} -gt 0 ]] && changed=true
|
${#subnets[@]} -gt 0 ]] && changed=true
|
||||||
|
|
||||||
if $changed; then
|
if $changed; then
|
||||||
local peer_rule
|
local peer_rule
|
||||||
peer_rule=$(peers::get_meta "$name" "rule")
|
peer_rule=$(peers::get_meta "$name" "rule")
|
||||||
|
|
@ -212,15 +208,7 @@ function cmd::block::run() {
|
||||||
block::restore_rules_for "$name" "$client_ip"
|
block::restore_rules_for "$name" "$client_ip"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Record history — derive block type from what was blocked
|
|
||||||
local btype="specific"
|
|
||||||
[[ ${#services[@]} -gt 0 ]] && btype="${services[0]}"
|
|
||||||
[[ ${#ips[@]} -gt 0 ]] && btype="ip"
|
|
||||||
[[ ${#subnets[@]} -gt 0 ]] && btype="subnet"
|
|
||||||
[[ ${#ports[@]} -gt 0 ]] && btype="port"
|
|
||||||
cmd::block::_record_history "$name" "$btype" "manual" "$reason"
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,25 +270,4 @@ function cmd::block::_block_all() {
|
||||||
block::set_direct "$name" "$client_ip" "true"
|
block::set_direct "$name" "$client_ip" "true"
|
||||||
|
|
||||||
$quiet || log::wg_success "${name} has been blocked."
|
$quiet || log::wg_success "${name} has been blocked."
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::block::_record_history() {
|
|
||||||
local name="${1:-}" block_type="${2:-full}" \
|
|
||||||
triggered_by="${3:-manual}" reason="${4:-}"
|
|
||||||
|
|
||||||
local endpoint
|
|
||||||
endpoint=$(json::peer_history_lookup "$name" 2>/dev/null || true)
|
|
||||||
|
|
||||||
# endpoint_cache lookup
|
|
||||||
local ep_cache
|
|
||||||
ep_cache=$(json::endpoint_cache_get "$(ctx::endpoint_cache)" "$name" 2>/dev/null || true)
|
|
||||||
|
|
||||||
json::block_history_record \
|
|
||||||
"$(ctx::block_history)" \
|
|
||||||
"$name" \
|
|
||||||
"$block_type" \
|
|
||||||
"$triggered_by" \
|
|
||||||
"$reason" \
|
|
||||||
"${ep_cache:-}" \
|
|
||||||
2>/dev/null > /dev/null || true
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# commands/export.command.sh
|
|
||||||
|
|
||||||
function cmd::export::on_load() {
|
|
||||||
flag::register --peer
|
|
||||||
flag::register --identity
|
|
||||||
flag::register --all
|
|
||||||
flag::register --out
|
|
||||||
flag::register --conf-only
|
|
||||||
flag::register --meta-only
|
|
||||||
flag::register --no-config
|
|
||||||
flag::register --no-peers
|
|
||||||
flag::register --force
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::export::help() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: wgctl export [options]
|
|
||||||
|
|
||||||
Export wgctl data as a portable JSON bundle.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--peer <name> Export a single peer (conf, meta, groups, identity, blocks)
|
|
||||||
--identity <name> Export an identity
|
|
||||||
--all Full backup (all peers, rules, identities, groups, etc.)
|
|
||||||
--out <file> Write to file instead of stdout
|
|
||||||
--conf-only Export peer conf only (with --peer)
|
|
||||||
--meta-only Export peer meta only (with --peer)
|
|
||||||
--no-config Skip wgctl.json (with --all)
|
|
||||||
--no-peers Skip peer confs (with --all)
|
|
||||||
--force Overwrite existing output file
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
wgctl export --peer phone-nuno
|
|
||||||
wgctl export --peer phone-nuno --out phone-nuno.json
|
|
||||||
wgctl export --identity nuno --out nuno.json
|
|
||||||
wgctl export --all --out backup.json
|
|
||||||
wgctl export --all --no-config --out data-only.json
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::export::run() {
|
|
||||||
local peer="" identity="" all=false out=""
|
|
||||||
local conf_only=false meta_only=false
|
|
||||||
local no_config=false no_peers=false force=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--peer) peer="$2"; shift 2 ;;
|
|
||||||
--identity) identity="$2"; shift 2 ;;
|
|
||||||
--all) all=true; shift ;;
|
|
||||||
--out) out="$2"; shift 2 ;;
|
|
||||||
--conf-only) conf_only=true; shift ;;
|
|
||||||
--meta-only) meta_only=true; shift ;;
|
|
||||||
--no-config) no_config=true; shift ;;
|
|
||||||
--no-peers) no_peers=true; shift ;;
|
|
||||||
--force) force=true; shift ;;
|
|
||||||
--help) cmd::export::help; return ;;
|
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Validate
|
|
||||||
local mode_count=0
|
|
||||||
[[ -n "$peer" ]] && (( mode_count++ )) || true
|
|
||||||
[[ -n "$identity" ]] && (( mode_count++ )) || true
|
|
||||||
$all && (( mode_count++ )) || true
|
|
||||||
|
|
||||||
if [[ "$mode_count" -eq 0 ]]; then
|
|
||||||
log::error "Specify --peer, --identity, or --all"
|
|
||||||
cmd::export::help
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ "$mode_count" -gt 1 ]]; then
|
|
||||||
log::error "Only one of --peer, --identity, --all can be used at a time"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check output file
|
|
||||||
if [[ -n "$out" && -f "$out" && ! $force ]]; then
|
|
||||||
log::error "Output file already exists: ${out} (use --force to overwrite)"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
local json=""
|
|
||||||
if [[ -n "$peer" ]]; then
|
|
||||||
json=$(cmd::export::_peer "$peer" "$conf_only" "$meta_only") || return 1
|
|
||||||
elif [[ -n "$identity" ]]; then
|
|
||||||
json=$(cmd::export::_identity "$identity") || return 1
|
|
||||||
elif $all; then
|
|
||||||
json=$(cmd::export::_full "$no_config" "$no_peers") || return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$out" ]]; then
|
|
||||||
echo "$json" > "$out"
|
|
||||||
log::wg_success "Exported to ${out}"
|
|
||||||
else
|
|
||||||
echo "$json"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Peer export
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::export::_peer() {
|
|
||||||
local name="${1:-}" conf_only="${2:-false}" meta_only="${3:-false}"
|
|
||||||
|
|
||||||
peers::require_exists "$name" || return 1
|
|
||||||
|
|
||||||
local conf_file
|
|
||||||
conf_file="$(ctx::clients)/${name}.conf"
|
|
||||||
[[ ! -f "$conf_file" ]] && log::error "Client conf not found: ${conf_file}" && return 1
|
|
||||||
|
|
||||||
local conf_b64
|
|
||||||
conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file")
|
|
||||||
|
|
||||||
if $conf_only; then
|
|
||||||
cmd::export::_envelope "peer_conf" \
|
|
||||||
"$(printf '{"name":"%s","conf":"%s"}' "$name" "$conf_b64")"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Meta
|
|
||||||
local meta_file meta_json="{}"
|
|
||||||
meta_file="$(ctx::meta)/${name}.meta"
|
|
||||||
[[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file")
|
|
||||||
|
|
||||||
if $meta_only; then
|
|
||||||
cmd::export::_envelope "peer_meta" \
|
|
||||||
"$(printf '{"name":"%s","meta":%s}' "$name" "$meta_json")"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Public key
|
|
||||||
local public_key=""
|
|
||||||
local key_file
|
|
||||||
key_file="$(ctx::clients)/${name}_public.key"
|
|
||||||
[[ -f "$key_file" ]] && public_key=$(cat "$key_file")
|
|
||||||
|
|
||||||
# IP
|
|
||||||
local ip
|
|
||||||
ip=$(peers::get_ip "$name")
|
|
||||||
|
|
||||||
# Type
|
|
||||||
local peer_type
|
|
||||||
peer_type=$(peers::get_type "$name" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
# Direct rule
|
|
||||||
local direct_rule
|
|
||||||
direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
# Identity
|
|
||||||
local identity
|
|
||||||
identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
# Groups
|
|
||||||
local -a group_list=()
|
|
||||||
while IFS= read -r g; do
|
|
||||||
[[ -n "$g" ]] && group_list+=("\"$g\"")
|
|
||||||
done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null)
|
|
||||||
local groups_json="[]"
|
|
||||||
[[ ${#group_list[@]} -gt 0 ]] && \
|
|
||||||
groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
|
|
||||||
|
|
||||||
# Blocks
|
|
||||||
local block_file is_blocked="false" block_json="null"
|
|
||||||
block_file="$(ctx::blocks)/${name}.block"
|
|
||||||
if [[ -f "$block_file" ]]; then
|
|
||||||
is_blocked="true"
|
|
||||||
block_json=$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file")
|
|
||||||
block_json="\"${block_json}\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
local peer_data
|
|
||||||
peer_data=$(printf \
|
|
||||||
'{"name":"%s","ip":"%s","type":"%s","public_key":"%s","conf":"%s","meta":%s,"identity":"%s","groups":%s,"direct_rule":"%s","blocks":{"is_blocked":%s,"block_file":%s}}' \
|
|
||||||
"$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \
|
|
||||||
"$meta_json" "$identity" "$groups_json" "$direct_rule" \
|
|
||||||
"$is_blocked" "$block_json")
|
|
||||||
|
|
||||||
cmd::export::_envelope "peer" "$peer_data"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Identity export
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::export::_identity() {
|
|
||||||
local name="${1:-}"
|
|
||||||
identity::require_exists "$name" || return 1
|
|
||||||
|
|
||||||
local id_file
|
|
||||||
id_file="$(ctx::identities)/${name}.identity"
|
|
||||||
local id_json
|
|
||||||
id_json=$(cat "$id_file")
|
|
||||||
|
|
||||||
cmd::export::_envelope "identity" \
|
|
||||||
"$(printf '{"name":"%s","identity":%s}' "$name" "$id_json")"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Full backup
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::export::_full() {
|
|
||||||
local no_config="${1:-false}" no_peers="${2:-false}"
|
|
||||||
local version
|
|
||||||
version=$(wgctl::version 2>/dev/null || echo "unknown")
|
|
||||||
|
|
||||||
python3 "$(ctx::json_helper)" export_full \
|
|
||||||
"$(ctx::clients)" \
|
|
||||||
"$(ctx::meta)" \
|
|
||||||
"$(ctx::rules)" \
|
|
||||||
"$(ctx::identities)" \
|
|
||||||
"$(ctx::groups)" \
|
|
||||||
"$(ctx::blocks)" \
|
|
||||||
"$(ctx::block_history)" \
|
|
||||||
"$(ctx::config_file)" \
|
|
||||||
"$(ctx::policies)" \
|
|
||||||
"$(ctx::subnets)" \
|
|
||||||
"$(ctx::net)" \
|
|
||||||
"$(ctx::hosts)" \
|
|
||||||
"$no_config" \
|
|
||||||
"$no_peers" \
|
|
||||||
"$version" \
|
|
||||||
2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper — peer data without envelope (used by full backup)
|
|
||||||
function cmd::export::_peer_data() {
|
|
||||||
local name="${1:-}"
|
|
||||||
local conf_file
|
|
||||||
conf_file="$(ctx::clients)/${name}.conf"
|
|
||||||
[[ ! -f "$conf_file" ]] && return 0
|
|
||||||
|
|
||||||
local conf_b64
|
|
||||||
conf_b64=$(base64 -w 0 < "$conf_file" 2>/dev/null || base64 < "$conf_file")
|
|
||||||
|
|
||||||
local meta_file meta_json="{}"
|
|
||||||
meta_file="$(ctx::meta)/${name}.meta"
|
|
||||||
[[ -f "$meta_file" ]] && meta_json=$(cat "$meta_file")
|
|
||||||
|
|
||||||
local public_key=""
|
|
||||||
local key_file
|
|
||||||
key_file="$(ctx::clients)/${name}_public.key"
|
|
||||||
[[ -f "$key_file" ]] && public_key=$(cat "$key_file")
|
|
||||||
|
|
||||||
local ip
|
|
||||||
ip=$(peers::get_ip "$name")
|
|
||||||
|
|
||||||
local peer_type
|
|
||||||
peer_type=$(peers::get_type "$name" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
local direct_rule
|
|
||||||
direct_rule=$(peers::get_meta "$name" "rule" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
local identity
|
|
||||||
identity=$(peers::get_identity "$name" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
local -a group_list=()
|
|
||||||
while IFS= read -r g; do
|
|
||||||
[[ -n "$g" ]] && group_list+=("\"$g\"")
|
|
||||||
done < <(json::peer_groups "$(ctx::groups)" "$name" 2>/dev/null)
|
|
||||||
local groups_json="[]"
|
|
||||||
[[ ${#group_list[@]} -gt 0 ]] && \
|
|
||||||
groups_json="[$(printf '%s,' "${group_list[@]}" | sed 's/,$//')]"
|
|
||||||
|
|
||||||
local block_file is_blocked="false" block_json="null"
|
|
||||||
block_file="$(ctx::blocks)/${name}.block"
|
|
||||||
if [[ -f "$block_file" ]]; then
|
|
||||||
is_blocked="true"
|
|
||||||
block_json="\"$(base64 -w 0 < "$block_file" 2>/dev/null || base64 < "$block_file")\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf \
|
|
||||||
'{"name":"%s","ip":"%s","type":"%s","public_key":"%s","conf":"%s","meta":%s,"identity":"%s","groups":%s,"direct_rule":"%s","blocks":{"is_blocked":%s,"block_file":%s}}' \
|
|
||||||
"$name" "$ip" "$peer_type" "$public_key" "$conf_b64" \
|
|
||||||
"$meta_json" "$identity" "$groups_json" "$direct_rule" \
|
|
||||||
"$is_blocked" "$block_json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Envelope helper
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::export::_envelope() {
|
|
||||||
local export_type="${1:-}" data="${2:-}"
|
|
||||||
local version ts
|
|
||||||
version=$(wgctl::version 2>/dev/null || echo "unknown")
|
|
||||||
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
printf '{"wgctl_version":"%s","export_type":"%s","exported_at":"%s","data":%s}\n' \
|
|
||||||
"$version" "$export_type" "$ts" "$data"
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::export::_compact_json() {
|
|
||||||
local file="$1"
|
|
||||||
python3 -c "
|
|
||||||
import json, sys
|
|
||||||
try:
|
|
||||||
print(json.dumps(json.load(open('${file}'))))
|
|
||||||
except Exception as e:
|
|
||||||
print('{}', file=sys.stderr)
|
|
||||||
" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
@ -147,11 +147,6 @@ function cmd::group::list() {
|
||||||
log::section "Groups"
|
log::section "Groups"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if display::is_table "group_list"; then
|
|
||||||
cmd::group::_render_table "$data" "$w_name" "$w_desc"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS="|" read -r name desc total blocked; do
|
while IFS="|" read -r name desc total blocked; do
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
ui::group::list_row "$name" "$desc" "$total" "$blocked" "$w_name" "$w_desc"
|
ui::group::list_row "$name" "$desc" "$total" "$blocked" "$w_name" "$w_desc"
|
||||||
|
|
@ -228,17 +223,6 @@ function cmd::group::show() {
|
||||||
printf "\n"
|
printf "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::group::_render_table() {
|
|
||||||
local data="${1:-}" w_name="${2:-20}" w_desc="${3:-20}"
|
|
||||||
[[ -z "$data" ]] && return 0
|
|
||||||
|
|
||||||
ui::group::list_header_table
|
|
||||||
while IFS='|' read -r name desc total blocked; do
|
|
||||||
[[ -z "$name" ]] && continue
|
|
||||||
ui::group::list_row_table "$name" "$desc" "$total" "$blocked"
|
|
||||||
done <<< "$data"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Add
|
# Add
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -146,11 +146,6 @@ function cmd::hosts::list() {
|
||||||
log::section "Host Mappings"
|
log::section "Host Mappings"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if display::is_table "hosts_list"; then
|
|
||||||
cmd::hosts::_render_table "$data"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local last_type="" found=false
|
local last_type="" found=false
|
||||||
while IFS='|' read -r type key name desc tags; do
|
while IFS='|' read -r type key name desc tags; do
|
||||||
[[ -z "$type" ]] && continue
|
[[ -z "$type" ]] && continue
|
||||||
|
|
@ -172,15 +167,19 @@ function cmd::hosts::list() {
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::hosts::_render_table() {
|
# Table version (kept for future display config)
|
||||||
local data="${1:-}"
|
function cmd::hosts::_list_table() {
|
||||||
[[ -z "$data" ]] && return 0
|
local hosts_file="${1:-}"
|
||||||
|
printf "\n %-6s %-18s %-16s %-30s %s\n" \
|
||||||
ui::hosts::list_header_table
|
"TYPE" "KEY" "NAME" "DESCRIPTION" "TAGS"
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..80})"
|
||||||
|
|
||||||
while IFS='|' read -r type key name desc tags; do
|
while IFS='|' read -r type key name desc tags; do
|
||||||
[[ -z "$type" ]] && continue
|
[[ -z "$type" ]] && continue
|
||||||
ui::hosts::list_row_table "$type" "$key" "$name" "$desc" "$tags"
|
printf " %-6s %-18s %-16s %-30s %s\n" \
|
||||||
done <<< "$data"
|
"$type" "$key" "$name" "${desc:-—}" "${tags:-—}"
|
||||||
|
done < <(json::hosts_list "$hosts_file" 2>/dev/null)
|
||||||
|
printf "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -111,12 +111,7 @@ function cmd::identity::_list() {
|
||||||
log::info "No identities found. Run 'wgctl identity migrate' to create from existing peers."
|
log::info "No identities found. Run 'wgctl identity migrate' to create from existing peers."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if display::is_table "identity_list"; then
|
|
||||||
cmd::identity::_render_table "$data"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
while IFS='|' read -r name peer_count types rules policy; do
|
while IFS='|' read -r name peer_count types rules policy; do
|
||||||
local rules_display
|
local rules_display
|
||||||
|
|
@ -190,21 +185,6 @@ function cmd::identity::_show() {
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::identity::_render_table() {
|
|
||||||
local data="${1:-}"
|
|
||||||
[[ -z "$data" ]] && return 0
|
|
||||||
|
|
||||||
printf "\n %-20s %-8s %-20s %s\n" "NAME" "PEERS" "RULES" "POLICY"
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
|
||||||
while IFS='|' read -r name peer_count types rules policy; do
|
|
||||||
[[ -z "$name" ]] && continue
|
|
||||||
local rules_display
|
|
||||||
rules_display=$(echo "$rules" | sed 's/,/, /g')
|
|
||||||
ui::identity::list_row_table "$name" "$peer_count" "$rules_display" "$policy"
|
|
||||||
done <<< "$data"
|
|
||||||
printf " %s\n\n" "$(printf '─%.0s' {1..65})"
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::identity::_device_status() {
|
function cmd::identity::_device_status() {
|
||||||
local peer_name="${1:-}"
|
local peer_name="${1:-}"
|
||||||
local -n _handshakes="${2:-__empty_map}"
|
local -n _handshakes="${2:-__empty_map}"
|
||||||
|
|
|
||||||
|
|
@ -1,288 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# commands/import.command.sh
|
|
||||||
|
|
||||||
function cmd::import::on_load() {
|
|
||||||
flag::register --file
|
|
||||||
flag::register --peer
|
|
||||||
flag::register --dry-run
|
|
||||||
flag::register --force
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::import::help() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: wgctl import --file <file> [options]
|
|
||||||
|
|
||||||
Import a wgctl JSON export bundle.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--file <path> Path to export bundle (required)
|
|
||||||
--peer <name> Import only this peer from a full backup
|
|
||||||
--dry-run Show what would be imported without making changes
|
|
||||||
--force Overwrite existing data
|
|
||||||
|
|
||||||
Export types handled:
|
|
||||||
peer Single peer bundle
|
|
||||||
peer_conf Peer conf only
|
|
||||||
peer_meta Peer meta only
|
|
||||||
identity Identity bundle
|
|
||||||
full Full backup
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
wgctl import --file backup.json
|
|
||||||
wgctl import --file backup.json --peer phone-nuno
|
|
||||||
wgctl import --file phone-nuno.json --dry-run
|
|
||||||
wgctl import --file backup.json --force
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::import::run() {
|
|
||||||
local file="" peer="" dry_run=false force=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--file) file="$2"; shift 2 ;;
|
|
||||||
--peer) peer="$2"; shift 2 ;;
|
|
||||||
--dry-run) dry_run=true; shift ;;
|
|
||||||
--force) force=true; shift ;;
|
|
||||||
--help) cmd::import::help; return ;;
|
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -z "$file" ]] && log::error "Missing required flag: --file" && return 1
|
|
||||||
[[ ! -f "$file" ]] && log::error "File not found: ${file}" && return 1
|
|
||||||
|
|
||||||
# Read export metadata via Python
|
|
||||||
local export_type export_version
|
|
||||||
export_type=$(json::import_get_field \
|
|
||||||
"$file" "export_type" 2>/dev/null)
|
|
||||||
export_version=$(json::import_get_field \
|
|
||||||
"$file" "wgctl_version" 2>/dev/null)
|
|
||||||
|
|
||||||
[[ -z "$export_type" ]] && \
|
|
||||||
log::error "Invalid export file: ${file}" && return 1
|
|
||||||
|
|
||||||
# Version check
|
|
||||||
local current_version
|
|
||||||
current_version=$(wgctl::version 2>/dev/null || echo "unknown")
|
|
||||||
if [[ "$export_version" != "$current_version" ]]; then
|
|
||||||
log::wg_warning "Export version (${export_version}) differs from current (${current_version})"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log::section "wgctl Import"
|
|
||||||
printf " File: %s\n" "$file"
|
|
||||||
printf " Type: %s\n" "$export_type"
|
|
||||||
$dry_run && printf " Mode: \033[2mdry run\033[0m\n"
|
|
||||||
printf "\n"
|
|
||||||
|
|
||||||
case "$export_type" in
|
|
||||||
peer) cmd::import::_peer "$file" "$dry_run" "$force" ;;
|
|
||||||
peer_conf) cmd::import::_peer_conf "$file" "$dry_run" "$force" ;;
|
|
||||||
peer_meta) cmd::import::_peer_meta "$file" "$dry_run" "$force" ;;
|
|
||||||
identity) cmd::import::_identity "$file" "$dry_run" "$force" ;;
|
|
||||||
full)
|
|
||||||
if [[ -n "$peer" ]]; then
|
|
||||||
cmd::import::_peer_from_full "$file" "$peer" "$dry_run" "$force"
|
|
||||||
else
|
|
||||||
cmd::import::_full "$file" "$dry_run" "$force"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log::error "Unknown export type: ${export_type}"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Helpers
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::import::_print_results() {
|
|
||||||
while IFS= read -r line; do
|
|
||||||
[[ -z "$line" ]] && continue
|
|
||||||
case "$line" in
|
|
||||||
error:*) log::error "${line#error:}" ;;
|
|
||||||
skip:*) printf " \033[2mskip: %s (already exists)\033[0m\n" "${line#skip:}" ;;
|
|
||||||
peer:*) printf " ✓ peer: %s\n" "${line#peer:}" ;;
|
|
||||||
group:*) printf " ✓ group: %s\n" "${line#group:}" ;;
|
|
||||||
*) printf " ✓ %s\n" "$line" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Peer import
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::import::_peer() {
|
|
||||||
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
|
||||||
|
|
||||||
local name
|
|
||||||
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
|
||||||
[[ -z "$name" ]] && log::error "Could not read peer name from export" && return 1
|
|
||||||
|
|
||||||
$dry_run && \
|
|
||||||
printf " \033[2m[dry-run]\033[0m Would import peer '%s'\n" "$name" && return 0
|
|
||||||
|
|
||||||
local err
|
|
||||||
err=$(json::import_peer \
|
|
||||||
"$file" "data" "$name" \
|
|
||||||
"$(ctx::clients)" "$(ctx::meta)" \
|
|
||||||
"$(ctx::groups)" "$(ctx::blocks)" \
|
|
||||||
"$($force && echo true || echo false)" \
|
|
||||||
2>&1 >/dev/null) || { log::error "$err"; return 1; }
|
|
||||||
|
|
||||||
json::import_peer \
|
|
||||||
"$file" "data" "$name" \
|
|
||||||
"$(ctx::clients)" "$(ctx::meta)" \
|
|
||||||
"$(ctx::groups)" "$(ctx::blocks)" \
|
|
||||||
"$($force && echo true || echo false)" \
|
|
||||||
2>/dev/null | cmd::import::_print_results
|
|
||||||
|
|
||||||
log::wg_success "Imported peer '${name}'"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Peer conf only
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::import::_peer_conf() {
|
|
||||||
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
|
||||||
|
|
||||||
local name
|
|
||||||
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
|
||||||
local conf_file="$(ctx::clients)/${name}.conf"
|
|
||||||
|
|
||||||
if [[ -f "$conf_file" ]] && ! $force; then
|
|
||||||
log::error "Peer conf '${name}' already exists. Use --force to overwrite."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
$dry_run && \
|
|
||||||
printf " \033[2m[dry-run]\033[0m Would import conf for '%s'\n" "$name" && return 0
|
|
||||||
|
|
||||||
python3 -c "
|
|
||||||
import json, base64
|
|
||||||
d = json.load(open('${file}'))
|
|
||||||
conf = base64.b64decode(d['data']['conf']).decode()
|
|
||||||
open('${conf_file}', 'w').write(conf)
|
|
||||||
" 2>/dev/null || { log::error "Failed to write conf"; return 1; }
|
|
||||||
|
|
||||||
printf " ✓ conf\n"
|
|
||||||
log::wg_success "Imported conf for '${name}'"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Peer meta only
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::import::_peer_meta() {
|
|
||||||
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
|
||||||
|
|
||||||
local name
|
|
||||||
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
|
||||||
|
|
||||||
[[ ! -f "$(ctx::clients)/${name}.conf" ]] && \
|
|
||||||
log::error "Peer '${name}' does not exist — import the peer conf first" && return 1
|
|
||||||
|
|
||||||
$dry_run && \
|
|
||||||
printf " \033[2m[dry-run]\033[0m Would import meta for '%s'\n" "$name" && return 0
|
|
||||||
|
|
||||||
python3 -c "
|
|
||||||
import json
|
|
||||||
d = json.load(open('${file}'))
|
|
||||||
meta = d['data']['meta']
|
|
||||||
open('$(ctx::meta)/${name}.meta', 'w').write(json.dumps(meta, indent=2))
|
|
||||||
" 2>/dev/null || { log::error "Failed to write meta"; return 1; }
|
|
||||||
|
|
||||||
printf " ✓ meta\n"
|
|
||||||
log::wg_success "Imported meta for '${name}'"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Identity import
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::import::_identity() {
|
|
||||||
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
|
||||||
|
|
||||||
local name
|
|
||||||
name=$(json::import_get_field "$file" "data" "name" 2>/dev/null)
|
|
||||||
|
|
||||||
$dry_run && \
|
|
||||||
printf " \033[2m[dry-run]\033[0m Would import identity '%s'\n" "$name" && return 0
|
|
||||||
|
|
||||||
local err
|
|
||||||
err=$(json::import_identity \
|
|
||||||
"$file" "$name" \
|
|
||||||
"$(ctx::identities)" "$(ctx::clients)" \
|
|
||||||
"$($force && echo true || echo false)" \
|
|
||||||
2>&1 >/dev/null) || { log::error "$err"; return 1; }
|
|
||||||
|
|
||||||
json::import_identity \
|
|
||||||
"$file" "$name" \
|
|
||||||
"$(ctx::identities)" "$(ctx::clients)" \
|
|
||||||
"$($force && echo true || echo false)" \
|
|
||||||
2>/dev/null | cmd::import::_print_results
|
|
||||||
|
|
||||||
log::wg_success "Imported identity '${name}'"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Single peer from full backup
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::import::_peer_from_full() {
|
|
||||||
local file="${1:-}" name="${2:-}" dry_run="${3:-false}" force="${4:-false}"
|
|
||||||
|
|
||||||
$dry_run && \
|
|
||||||
printf " \033[2m[dry-run]\033[0m Would import peer '%s' from backup\n" "$name" && return 0
|
|
||||||
|
|
||||||
local err
|
|
||||||
err=$(json::import_peer \
|
|
||||||
"$file" "peers" "$name" \
|
|
||||||
"$(ctx::clients)" "$(ctx::meta)" \
|
|
||||||
"$(ctx::groups)" "$(ctx::blocks)" \
|
|
||||||
"$($force && echo true || echo false)" \
|
|
||||||
2>&1 >/dev/null) || { log::error "$err"; return 1; }
|
|
||||||
|
|
||||||
json::import_peer \
|
|
||||||
"$file" "peers" "$name" \
|
|
||||||
"$(ctx::clients)" "$(ctx::meta)" \
|
|
||||||
"$(ctx::groups)" "$(ctx::blocks)" \
|
|
||||||
"$($force && echo true || echo false)" \
|
|
||||||
2>/dev/null | cmd::import::_print_results
|
|
||||||
|
|
||||||
log::wg_success "Imported peer '${name}' from backup"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Full backup import
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function cmd::import::_full() {
|
|
||||||
local file="${1:-}" dry_run="${2:-false}" force="${3:-false}"
|
|
||||||
|
|
||||||
local peer_count
|
|
||||||
peer_count=$(json::import_get_field \
|
|
||||||
"$file" "data" "peers" 2>/dev/null | python3 -c \
|
|
||||||
"import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "?")
|
|
||||||
|
|
||||||
printf " Importing full backup (%s peers)...\n\n" "$peer_count"
|
|
||||||
|
|
||||||
$dry_run && \
|
|
||||||
log::wg_warning "Dry run — no changes would be made" && return 0
|
|
||||||
|
|
||||||
json::import_full \
|
|
||||||
"$file" \
|
|
||||||
"$(ctx::clients)" "$(ctx::meta)" \
|
|
||||||
"$(ctx::rules)" "$(ctx::identities)" \
|
|
||||||
"$(ctx::groups)" "$(ctx::blocks)" \
|
|
||||||
"$(ctx::policies)" "$(ctx::subnets)" \
|
|
||||||
"$(ctx::net)" "$(ctx::hosts)" \
|
|
||||||
"$($force && echo true || echo false)" \
|
|
||||||
2>/dev/null | cmd::import::_print_results
|
|
||||||
|
|
||||||
log::wg_success "Import complete"
|
|
||||||
}
|
|
||||||
|
|
@ -5,8 +5,6 @@
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::on_load() {
|
function cmd::list::on_load() {
|
||||||
command::mixin json_output
|
|
||||||
|
|
||||||
load_module identity
|
load_module identity
|
||||||
load_module ui
|
load_module ui
|
||||||
|
|
||||||
|
|
@ -21,9 +19,7 @@ function cmd::list::on_load() {
|
||||||
flag::register --allowed
|
flag::register --allowed
|
||||||
flag::register --detailed
|
flag::register --detailed
|
||||||
flag::register --name
|
flag::register --name
|
||||||
|
command::mixin json_output
|
||||||
# Mutually exclusive filter groups
|
|
||||||
flag::exclusive --online --offline --blocked --restricted --allowed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -230,11 +226,8 @@ function cmd::list::run() {
|
||||||
|
|
||||||
case "$style" in
|
case "$style" in
|
||||||
table) cmd::list::_render_table ;;
|
table) cmd::list::_render_table ;;
|
||||||
compact) display::render "peer_list" "$collected_rows" \
|
compact) cmd::list::_render_compact "$collected_rows" ;;
|
||||||
"cmd::list::_render_compact" "cmd::list::_render_table" ;;
|
*) cmd::list::_render_compact "$collected_rows" ;;
|
||||||
|
|
||||||
*) display::render "peer_list" "$collected_rows" \
|
|
||||||
"cmd::list::_render_compact" "cmd::list::_render_table" ;;
|
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -353,63 +346,19 @@ function cmd::list::_render_compact() {
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_render_table() {
|
function cmd::list::_render_table() {
|
||||||
local rows="${1:-}"
|
declare -A rule_counts=() group_counts=()
|
||||||
[[ -z "$rows" ]] && log::wg_warning "No results found" && return 0
|
_list_header_printed=false
|
||||||
|
|
||||||
# Measure column widths from data (same as compact)
|
cmd::list::_iter_confs_table
|
||||||
local w_name=16 w_ip=13 w_type=8 w_rule=10 w_group=10 w_status=10 w_last=20
|
|
||||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
|
||||||
[[ -z "$name" ]] && continue
|
|
||||||
(( ${#name} > w_name )) && w_name=${#name}
|
|
||||||
(( ${#ip} > w_ip )) && w_ip=${#ip}
|
|
||||||
(( ${#type} > w_type )) && w_type=${#type}
|
|
||||||
(( ${#rule} > w_rule )) && w_rule=${#rule}
|
|
||||||
(( ${#group} > w_group )) && w_group=${#group}
|
|
||||||
(( ${#last_seen} > w_last )) && w_last=${#last_seen}
|
|
||||||
local cs
|
|
||||||
cs=$(printf "%s" "$status" | sed 's/\x1b\[[0-9;]*m//g')
|
|
||||||
(( ${#cs} > w_status )) && w_status=${#cs}
|
|
||||||
done <<< "$rows"
|
|
||||||
(( w_name += 2 )); (( w_ip += 2 ))
|
|
||||||
(( w_type += 2 )); (( w_rule += 2 ))
|
|
||||||
(( w_group += 2 )); (( w_last += 2 ))
|
|
||||||
|
|
||||||
# Header
|
if [[ "$_list_header_printed" == "true" ]]; then
|
||||||
printf "\n %-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %-${w_status}s %s\n" \
|
cmd::list::_render_footer $has_groups
|
||||||
"NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
|
local group_summary=""
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..115})"
|
cmd::list::_build_group_summary
|
||||||
|
printf "\n Showing peers\n\n"
|
||||||
# Rows
|
else
|
||||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
log::wg_warning "No results found"
|
||||||
[[ -z "$name" ]] && continue
|
fi
|
||||||
local clean_status
|
|
||||||
clean_status=$(echo "$status" | sed 's/\x1b\[[0-9;]*m//g')
|
|
||||||
local status_pad_n=$(( w_status - ${#clean_status} ))
|
|
||||||
[[ $status_pad_n -lt 0 ]] && status_pad_n=0
|
|
||||||
|
|
||||||
local row_color status_color
|
|
||||||
row_color=$(ui::peer::_row_color "$is_blocked" "$is_restricted" "$clean_status")
|
|
||||||
status_color=$(ui::peer::status_color "$is_blocked" "$is_restricted" "$clean_status")
|
|
||||||
|
|
||||||
local status_colored="${status_color}${clean_status}\033[0m"
|
|
||||||
|
|
||||||
local last_seen_colored="$last_seen"
|
|
||||||
[[ -n "$row_color" ]] && last_seen_colored="${row_color}${last_seen}\033[0m" \
|
|
||||||
|| last_seen_colored="${status_color}${last_seen}\033[0m"
|
|
||||||
|
|
||||||
if [[ -n "$row_color" ]]; then
|
|
||||||
printf " %b%-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %-${w_status}s %s\033[0m\n" \
|
|
||||||
"$row_color" "$name" "$ip" "$type" "$rule" "$group" "$clean_status" "$last_seen"
|
|
||||||
else
|
|
||||||
printf " %-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %b%*s\033[0m %b\n" \
|
|
||||||
"$name" "$ip" "$type" "$rule" "$group" \
|
|
||||||
"$status_color${clean_status}" "$status_pad_n" "" \
|
|
||||||
"$last_seen_colored"
|
|
||||||
fi
|
|
||||||
done <<< "$rows"
|
|
||||||
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..115})"
|
|
||||||
cmd::list::_render_summary_from_rows "$rows"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::list::_iter_confs_table() {
|
function cmd::list::_iter_confs_table() {
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ function cmd::logs::on_load() {
|
||||||
flag::register --ascending
|
flag::register --ascending
|
||||||
flag::register --descending
|
flag::register --descending
|
||||||
flag::register --resolved
|
flag::register --resolved
|
||||||
|
|
||||||
flag::exclusive --ascending --descending
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::help() {
|
function cmd::logs::help() {
|
||||||
|
|
@ -163,11 +161,6 @@ function cmd::logs::show() {
|
||||||
[[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1
|
[[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $fw_only && $wg_only; then
|
|
||||||
fw_only=false
|
|
||||||
wg_only=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
if $follow; then
|
if $follow; then
|
||||||
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
|
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ function cmd::net::list() {
|
||||||
log::wg_warning "No services configured"
|
log::wg_warning "No services configured"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Measure column widths
|
# Measure column widths
|
||||||
local w_name=12 w_ip=13 w_ports=16
|
local w_name=12 w_ip=13 w_ports=16
|
||||||
while IFS="|" read -r name ip desc tags ports; do
|
while IFS="|" read -r name ip desc tags ports; do
|
||||||
|
|
@ -144,11 +144,6 @@ function cmd::net::list() {
|
||||||
|
|
||||||
log::section "Network Services"
|
log::section "Network Services"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if display::is_table "net_list"; then
|
|
||||||
cmd::net::_render_table "$filtered_data"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS="|" read -r name ip desc tags ports; do
|
while IFS="|" read -r name ip desc tags ports; do
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
|
|
@ -167,17 +162,6 @@ function cmd::net::list() {
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::net::_render_table() {
|
|
||||||
local data="${1:-}"
|
|
||||||
[[ -z "$data" ]] && return 0
|
|
||||||
|
|
||||||
ui::net::list_header_table
|
|
||||||
while IFS='|' read -r name ip desc tags port_count; do
|
|
||||||
[[ -z "$name" ]] && continue
|
|
||||||
ui::net::list_row_table "$name" "$ip" "$desc" "$tags" "$port_count"
|
|
||||||
done <<< "$data"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Show
|
# Show
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -113,11 +113,6 @@ function cmd::policy::_list() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if display::is_table "policy_list"; then
|
|
||||||
cmd::policy::_render_table "$data"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
while IFS='|' read -r name tunnel default_rule strict auto desc; do
|
while IFS='|' read -r name tunnel default_rule strict auto desc; do
|
||||||
ui::policy::list_row "$name" "$default_rule" "$strict" "$auto"
|
ui::policy::list_row "$name" "$default_rule" "$strict" "$auto"
|
||||||
|
|
@ -125,19 +120,6 @@ function cmd::policy::_list() {
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::policy::_render_table() {
|
|
||||||
local data="${1:-}"
|
|
||||||
[[ -z "$data" ]] && return 0
|
|
||||||
|
|
||||||
ui::policy::list_header_table
|
|
||||||
while IFS='|' read -r name tunnel default_rule strict auto desc; do
|
|
||||||
[[ -z "$name" ]] && continue
|
|
||||||
ui::policy::list_row_table "$name" "$tunnel" "$default_rule" "$strict" "$auto"
|
|
||||||
done <<< "$data"
|
|
||||||
printf "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function cmd::policy::_show() {
|
function cmd::policy::_show() {
|
||||||
local name=""
|
local name=""
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
|
|
|
||||||
|
|
@ -181,11 +181,6 @@ function cmd::rule::list() {
|
||||||
log::section "Firewall Rules"
|
log::section "Firewall Rules"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if display::is_table "rule_list"; then
|
|
||||||
cmd::rule::_render_table "$data"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local current_group="" printing_base=false found_any=false
|
local current_group="" printing_base=false found_any=false
|
||||||
|
|
||||||
while IFS="|" read -r name desc n_allows n_blocks \
|
while IFS="|" read -r name desc n_allows n_blocks \
|
||||||
|
|
@ -245,18 +240,6 @@ function cmd::rule::list() {
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::rule::_render_table() {
|
|
||||||
local data="${1:-}"
|
|
||||||
[[ -z "$data" ]] && return 0
|
|
||||||
|
|
||||||
ui::rule::list_header_table
|
|
||||||
while IFS='|' read -r name desc n_allows n_blocks peer_count extends is_base group; do
|
|
||||||
[[ -z "$name" ]] && continue
|
|
||||||
ui::rule::list_row_table "$name" "$n_allows" "$n_blocks" "$peer_count" "$extends" "$group"
|
|
||||||
done <<< "$data"
|
|
||||||
printf "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Show
|
# Show
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,6 @@ function cmd::subnet::_list() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if display::is_table "subnet_list"; then
|
|
||||||
cmd::subnet::_render_table "$data"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
local prev_group=""
|
local prev_group=""
|
||||||
while IFS='|' read -r display_name subnet type_key tunnel_mode desc is_group group_parent; do
|
while IFS='|' read -r display_name subnet type_key tunnel_mode desc is_group group_parent; do
|
||||||
|
|
@ -125,18 +120,6 @@ function cmd::subnet::_list() {
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::subnet::_render_table() {
|
|
||||||
local data="${1:-}"
|
|
||||||
[[ -z "$data" ]] && return 0
|
|
||||||
|
|
||||||
ui::subnet::list_header_table
|
|
||||||
while IFS='|' read -r type cidr display_name tunnel desc is_group group_parent; do
|
|
||||||
[[ -z "$type" ]] && continue
|
|
||||||
ui::subnet::list_row_table "$type" "$cidr" "$tunnel" "$desc"
|
|
||||||
done <<< "$data"
|
|
||||||
printf "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::subnet::_maybe_group_separator() {
|
function cmd::subnet::_maybe_group_separator() {
|
||||||
local is_group="${1:-}" group_parent="${2:-}" prev_group="${3:-}"
|
local is_group="${1:-}" group_parent="${2:-}" prev_group="${3:-}"
|
||||||
if [[ "$is_group" == "true" && "$group_parent" != "$prev_group" && -n "$prev_group" ]]; then
|
if [[ "$is_group" == "true" && "$group_parent" != "$prev_group" && -n "$prev_group" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,6 @@ function cmd::test::run_all_integration_sections() {
|
||||||
cmd::test::section_config
|
cmd::test::section_config
|
||||||
cmd::test::section_rules
|
cmd::test::section_rules
|
||||||
cmd::test::section_groups
|
cmd::test::section_groups
|
||||||
cmd::test::section_block_unblock
|
|
||||||
cmd::test::section_audit
|
cmd::test::section_audit
|
||||||
cmd::test::section_logs
|
cmd::test::section_logs
|
||||||
cmd::test::section_fw
|
cmd::test::section_fw
|
||||||
|
|
@ -139,8 +138,6 @@ function cmd::test::run_all_integration_sections() {
|
||||||
cmd::test::section_peer_cmd
|
cmd::test::section_peer_cmd
|
||||||
cmd::test::section_group_purge
|
cmd::test::section_group_purge
|
||||||
cmd::test::section_logs_clean
|
cmd::test::section_logs_clean
|
||||||
cmd::test::section_display
|
|
||||||
cmd::test::section_export
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_list() {
|
function cmd::test::section_list() {
|
||||||
|
|
@ -177,8 +174,6 @@ function cmd::test::section_config() {
|
||||||
cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno
|
cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno
|
||||||
cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
|
cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
|
||||||
cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno
|
cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno
|
||||||
cmd::test::run_cmd "config migrate --dry-run" "" config migrate --dry-run
|
|
||||||
cmd::test::run_cmd_fails "config missing --name" config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_rules() {
|
function cmd::test::section_rules() {
|
||||||
|
|
@ -205,92 +200,6 @@ function cmd::test::section_groups() {
|
||||||
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
||||||
}
|
}
|
||||||
|
|
||||||
# function cmd::test::section_blocks() {
|
|
||||||
# test::section "Blocks"
|
|
||||||
# cmd::test::run_cmd "block --reason records history" "block-history" block --name guest-test --reason "test block" --force
|
|
||||||
# cmd::test::run_cmd_succeeds "unblock clears history" unblock --name guest-test --force
|
|
||||||
# }
|
|
||||||
|
|
||||||
function cmd::test::section_block_unblock() {
|
|
||||||
test::section "Block / Unblock"
|
|
||||||
|
|
||||||
# ── Setup fixture ──
|
|
||||||
local fixture="phone-testblock"
|
|
||||||
wgctl unblock --name "$fixture" --force >/dev/null 2>&1 || true
|
|
||||||
wgctl remove --name "$fixture" --force >/dev/null 2>&1 || true
|
|
||||||
wgctl add --name testblock --type phone >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
local history_file
|
|
||||||
history_file="$(ctx::block_history)/${fixture}.json"
|
|
||||||
|
|
||||||
# ── Block ──
|
|
||||||
echo "DEBUG about to run: $WGCTL_BINARY block --name $fixture --force" >&2
|
|
||||||
cmd::test::run_cmd "block peer" "blocked" block --name "$fixture" --force
|
|
||||||
cmd::test::run_cmd "block already blocked" "already" block --name "$fixture" --force
|
|
||||||
|
|
||||||
wgctl unblock --name "$fixture" --force >/dev/null 2>&1 || true
|
|
||||||
cmd::test::run_cmd "block with reason" "blocked" block --name "$fixture" --force \
|
|
||||||
--reason "test reason"
|
|
||||||
|
|
||||||
# ── Block history file created ──
|
|
||||||
[[ -f "$history_file" ]] && test::pass "block history file created" \
|
|
||||||
|| test::fail "block history file not created"
|
|
||||||
|
|
||||||
# ── Block history fields ──
|
|
||||||
if [[ -f "$history_file" ]]; then
|
|
||||||
local has_id has_blocked_at has_endpoint has_reason
|
|
||||||
has_id=$(python3 -c "
|
|
||||||
import json
|
|
||||||
d=json.load(open('$history_file'))
|
|
||||||
print('yes' if d['history'] and 'id' in d['history'][-1] else 'no')
|
|
||||||
" 2>/dev/null)
|
|
||||||
has_blocked_at=$(python3 -c "
|
|
||||||
import json
|
|
||||||
d=json.load(open('$history_file'))
|
|
||||||
print('yes' if d['history'] and d['history'][-1].get('blocked_at') else 'no')
|
|
||||||
" 2>/dev/null)
|
|
||||||
has_endpoint=$(python3 -c "
|
|
||||||
import json
|
|
||||||
d=json.load(open('$history_file'))
|
|
||||||
print('yes' if 'endpoint_at_block' in d['history'][-1] else 'no')
|
|
||||||
" 2>/dev/null)
|
|
||||||
has_reason=$(python3 -c "
|
|
||||||
import json
|
|
||||||
d=json.load(open('$history_file'))
|
|
||||||
print('yes' if d['history'][-1].get('reason') == 'test reason' else 'no')
|
|
||||||
" 2>/dev/null)
|
|
||||||
cmd::test::assert "history has id" "$has_id" "yes"
|
|
||||||
cmd::test::assert "history has blocked_at" "$has_blocked_at" "yes"
|
|
||||||
cmd::test::assert "history has endpoint" "$has_endpoint" "yes"
|
|
||||||
cmd::test::assert "history has reason" "$has_reason" "yes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Unblock ──
|
|
||||||
cmd::test::run_cmd "unblock peer" "unblocked" unblock --name "$fixture" --force \
|
|
||||||
--reason "test cleanup"
|
|
||||||
cmd::test::run_cmd "unblock not blocked" "not blocked" unblock --name "$fixture" --force
|
|
||||||
|
|
||||||
# ── Unblock history updated ──
|
|
||||||
if [[ -f "$history_file" ]]; then
|
|
||||||
local has_unblocked has_unblock_reason
|
|
||||||
has_unblocked=$(python3 -c "
|
|
||||||
import json
|
|
||||||
d=json.load(open('$history_file'))
|
|
||||||
print('yes' if d['history'] and d['history'][-1].get('unblocked_at') else 'no')
|
|
||||||
" 2>/dev/null)
|
|
||||||
has_unblock_reason=$(python3 -c "
|
|
||||||
import json
|
|
||||||
d=json.load(open('$history_file'))
|
|
||||||
print('yes' if d['history'][-1].get('unblock_reason') == 'test cleanup' else 'no')
|
|
||||||
" 2>/dev/null)
|
|
||||||
cmd::test::assert "history has unblocked_at" "$has_unblocked" "yes"
|
|
||||||
cmd::test::assert "history has unblock_reason" "$has_unblock_reason" "yes"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Teardown fixture ──
|
|
||||||
wgctl remove --name "$fixture" --force >/dev/null 2>&1 || true
|
|
||||||
rm -f "$history_file"
|
|
||||||
}
|
|
||||||
function cmd::test::section_audit() {
|
function cmd::test::section_audit() {
|
||||||
test::section "Audit"
|
test::section "Audit"
|
||||||
cmd::test::run_cmd_any "audit" "passed" audit
|
cmd::test::run_cmd_any "audit" "passed" audit
|
||||||
|
|
@ -457,90 +366,4 @@ function cmd::test::section_logs_clean() {
|
||||||
cmd::test::run_cmd "logs clean --force" \
|
cmd::test::run_cmd "logs clean --force" \
|
||||||
"keepalive" \
|
"keepalive" \
|
||||||
logs clean --force
|
logs clean --force
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::test::section_export() {
|
|
||||||
test::section "Export"
|
|
||||||
|
|
||||||
# Single peer export
|
|
||||||
cmd::test::run_cmd "export --peer" '"export_type":"peer"' export --peer phone-nuno
|
|
||||||
cmd::test::run_cmd "export --peer has name" '"name":"phone-nuno"' export --peer phone-nuno
|
|
||||||
cmd::test::run_cmd "export --peer has conf" '"conf":' export --peer phone-nuno
|
|
||||||
cmd::test::run_cmd "export --peer has identity" '"identity":"nuno"' export --peer phone-nuno
|
|
||||||
cmd::test::run_cmd "export --peer has groups" '"groups":' export --peer phone-nuno
|
|
||||||
cmd::test::run_cmd "export --peer has blocks" '"blocks":' export --peer phone-nuno
|
|
||||||
cmd::test::run_cmd "export --peer conf-only" '"export_type":"peer_conf"' export --peer phone-nuno --conf-only
|
|
||||||
cmd::test::run_cmd "export --peer meta-only" '"export_type":"peer_meta"' export --peer phone-nuno --meta-only
|
|
||||||
cmd::test::run_cmd_fails "export missing flag" export
|
|
||||||
# Identity export
|
|
||||||
cmd::test::run_cmd "export --identity" '"export_type":"identity"' export --identity nuno
|
|
||||||
|
|
||||||
# Full backup
|
|
||||||
cmd::test::run_cmd "export --all" '"export_type"' export --all
|
|
||||||
cmd::test::run_cmd "export --all is full" '"full"' export --all
|
|
||||||
cmd::test::run_cmd "export --all has peers" '"peers"' export --all
|
|
||||||
cmd::test::run_cmd "export --all has rules" '"rules"' export --all
|
|
||||||
cmd::test::run_cmd "export --all has identities" '"identities"' export --all
|
|
||||||
cmd::test::run_cmd "export --all has config" '"config"' export --all
|
|
||||||
cmd::test::run_cmd "export --all no-config" '"peers"' export --all --no-config
|
|
||||||
# --no-config should NOT have config section
|
|
||||||
local no_config_out
|
|
||||||
no_config_out=$(wgctl export --all --no-config 2>/dev/null)
|
|
||||||
if echo "$no_config_out" | grep -qF '"config":'; then
|
|
||||||
test::fail "export --all --no-config should not have config section"
|
|
||||||
else
|
|
||||||
test::pass "export --all --no-config has no config section"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export to file
|
|
||||||
local tmp_file="/tmp/wgctl_test_export_$$.json"
|
|
||||||
cmd::test::run_cmd "export --peer --out file" "Exported to" export --peer phone-nuno --out "$tmp_file"
|
|
||||||
[[ -f "$tmp_file" ]] && test::pass "export --out file exists" \
|
|
||||||
|| test::fail "export --out file not created"
|
|
||||||
rm -f "$tmp_file"
|
|
||||||
|
|
||||||
# Version in export
|
|
||||||
cmd::test::run_cmd "export has version" '"wgctl_version":' export --peer phone-nuno
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::test::section_display() {
|
|
||||||
test::section "Display config"
|
|
||||||
|
|
||||||
cmd::test::run_cmd "display config exists" "" \
|
|
||||||
config show --name phone-nuno # just check wgctl works, not display directly
|
|
||||||
|
|
||||||
# Test style via unit tests (see unit section)
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Helpers
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
function cmd::test::assert() {
|
|
||||||
local desc="${1:-}" result="${2:-}" expected="${3:-}"
|
|
||||||
if [[ "$result" == "$expected" ]]; then
|
|
||||||
test::pass "$desc"
|
|
||||||
else
|
|
||||||
test::fail "${desc} (expected '${expected}', got '${result}')"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::test::assert_true() {
|
|
||||||
local desc="${1:-}"
|
|
||||||
shift
|
|
||||||
if "$@" 2>/dev/null; then
|
|
||||||
test::pass "$desc"
|
|
||||||
else
|
|
||||||
test::fail "$desc (expected true, got false)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::test::assert_false() {
|
|
||||||
local desc="${1:-}"
|
|
||||||
shift
|
|
||||||
if ! "$@" 2>/dev/null; then
|
|
||||||
test::pass "$desc"
|
|
||||||
else
|
|
||||||
test::fail "$desc (expected false, got true)"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
@ -49,8 +49,6 @@ function cmd::test::run_all_unit_sections() {
|
||||||
cmd::test::unit_parse_since
|
cmd::test::unit_parse_since
|
||||||
cmd::test::unit_group_status
|
cmd::test::unit_group_status
|
||||||
cmd::test::unit_json_output
|
cmd::test::unit_json_output
|
||||||
cmd::test::unit_display
|
|
||||||
cmd::test::unit_version
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::unit_subnet() {
|
function cmd::test::unit_subnet() {
|
||||||
|
|
@ -227,8 +225,6 @@ function cmd::test::unit_group_status() {
|
||||||
function cmd::test::unit_json_output() {
|
function cmd::test::unit_json_output() {
|
||||||
test::section "Unit: JSON output"
|
test::section "Unit: JSON output"
|
||||||
|
|
||||||
command::_load_mixins 2>/dev/null || true
|
|
||||||
|
|
||||||
# json::envelope produces valid structure
|
# json::envelope produces valid structure
|
||||||
local result
|
local result
|
||||||
result=$(echo '{"peers":[]}' | json::envelope "list" "0")
|
result=$(echo '{"peers":[]}' | json::envelope "list" "0")
|
||||||
|
|
@ -239,11 +235,8 @@ function cmd::test::unit_json_output() {
|
||||||
|
|
||||||
# command::mixin registration
|
# command::mixin registration
|
||||||
load_command list
|
load_command list
|
||||||
cmd::test::assert_true "json_output mixin registered" \
|
cmd::test::assert_true "json_output mixin registered" "declare -f command::mixin::json_output::register >/dev/null 2>&1"
|
||||||
declare -f command::mixin::json_output::register >/dev/null 2>&1
|
cmd::test::assert_true "command::json accessor exists" "declare -f command::json >/dev/null 2>&1"
|
||||||
|
|
||||||
cmd::test::assert_true "command::json accessor exists" \
|
|
||||||
declare -f command::json >/dev/null 2>&1
|
|
||||||
|
|
||||||
# json::error_envelope
|
# json::error_envelope
|
||||||
local err_result
|
local err_result
|
||||||
|
|
@ -258,36 +251,4 @@ function cmd::test::unit_json_output() {
|
||||||
cmd::test::assert_true "command::json true" "command::json"
|
cmd::test::assert_true "command::json true" "command::json"
|
||||||
_COMMAND_JSON=false
|
_COMMAND_JSON=false
|
||||||
cmd::test::assert_false "command::json false" "command::json"
|
cmd::test::assert_false "command::json false" "command::json"
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::test::unit_display() {
|
|
||||||
test::section "Unit: display config"
|
|
||||||
|
|
||||||
load_module display
|
|
||||||
|
|
||||||
# Default style is compact
|
|
||||||
cmd::test::assert "display::style peer_list default" \
|
|
||||||
"$(display::style "peer_list")" "compact"
|
|
||||||
|
|
||||||
# is_compact returns true for compact
|
|
||||||
display::is_compact "peer_list" && \
|
|
||||||
test::pass "display::is_compact peer_list" || \
|
|
||||||
test::fail "display::is_compact peer_list"
|
|
||||||
|
|
||||||
# is_table returns false for compact
|
|
||||||
display::is_table "peer_list" && \
|
|
||||||
test::fail "display::is_table should be false for compact" || \
|
|
||||||
test::pass "display::is_table returns false for compact"
|
|
||||||
|
|
||||||
# Unknown view defaults to compact
|
|
||||||
cmd::test::assert "display::style unknown view" \
|
|
||||||
"$(display::style "nonexistent_view")" "compact"
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::test::unit_version() {
|
|
||||||
test::section "Unit: wgctl version"
|
|
||||||
local ver
|
|
||||||
ver=$(wgctl::version 2>/dev/null)
|
|
||||||
[[ -n "$ver" ]] && test::pass "wgctl::version returns value: $ver" \
|
|
||||||
|| test::fail "wgctl::version returns empty"
|
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +16,6 @@ function cmd::unblock::on_load() {
|
||||||
flag::register --subnet
|
flag::register --subnet
|
||||||
flag::register --all
|
flag::register --all
|
||||||
flag::register --service
|
flag::register --service
|
||||||
flag::register --reason
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -60,22 +59,20 @@ function cmd::unblock::run() {
|
||||||
local name="" identity="" type=""
|
local name="" identity="" type=""
|
||||||
local ips=() subnets=() ports=() services=()
|
local ips=() subnets=() ports=() services=()
|
||||||
local all=false quiet=false force=false
|
local all=false quiet=false force=false
|
||||||
local reason=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
--identity) identity="$2"; shift 2 ;;
|
--identity) identity="$2"; shift 2 ;;
|
||||||
--type) type="$2"; shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
--ip) ips+=("$2"); shift 2 ;;
|
--ip) ips+=("$2"); shift 2 ;;
|
||||||
--force) force=true; shift ;;
|
--force) force=true; shift ;;
|
||||||
--quiet) quiet=true; shift ;;
|
--quiet) quiet=true; shift ;;
|
||||||
--subnet) subnets+=("$2"); shift 2 ;;
|
--subnet) subnets+=("$2"); shift 2 ;;
|
||||||
--port) ports+=("$2"); shift 2 ;;
|
--port) ports+=("$2"); shift 2 ;;
|
||||||
--service) services+=("$2"); shift 2 ;;
|
--service) services+=("$2"); shift 2 ;;
|
||||||
--reason) reason="$2"; shift 2 ;;
|
--all) all=true; shift ;;
|
||||||
--all) all=true; shift ;;
|
--help) cmd::unblock::help; return ;;
|
||||||
--help) cmd::unblock::help; return ;;
|
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
cmd::unblock::help
|
cmd::unblock::help
|
||||||
|
|
@ -113,7 +110,6 @@ function cmd::unblock::run() {
|
||||||
|
|
||||||
if $all; then
|
if $all; then
|
||||||
cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet"
|
cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet"
|
||||||
cmd::unblock::_record_history "$name" "manual" "$reason"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -184,9 +180,6 @@ function cmd::unblock::run() {
|
||||||
done
|
done
|
||||||
|
|
||||||
block::cleanup "$name"
|
block::cleanup "$name"
|
||||||
|
|
||||||
# Record unblock for specific rules
|
|
||||||
cmd::unblock::_record_history "$name" "manual" "$reason"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,15 +248,4 @@ function cmd::unblock::_unblock_all() {
|
||||||
|
|
||||||
$quiet || log::wg_success "${name} has been unblocked."
|
$quiet || log::wg_success "${name} has been unblocked."
|
||||||
return 0
|
return 0
|
||||||
}
|
|
||||||
|
|
||||||
function cmd::unblock::_record_history() {
|
|
||||||
local name="${1:-}" unblocked_by="${2:-manual}" reason="${3:-}"
|
|
||||||
|
|
||||||
json::block_history_unblock \
|
|
||||||
"$(ctx::block_history)" \
|
|
||||||
"$name" \
|
|
||||||
"$unblocked_by" \
|
|
||||||
"$reason" \
|
|
||||||
2>/dev/null > /dev/null || true
|
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,6 @@ declare -A _LOADED_COMMANDS=()
|
||||||
|
|
||||||
readonly _COMMAND_NAMESPACE="cmd"
|
readonly _COMMAND_NAMESPACE="cmd"
|
||||||
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
|
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
|
||||||
_CURRENT_LOADING_CMD=""
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Helpers
|
# Helpers
|
||||||
|
|
@ -37,57 +36,13 @@ function command::exists() { command::has_function "$1" run; }
|
||||||
# Runner
|
# Runner
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# function command::run() {
|
|
||||||
# local cmd="$1"
|
|
||||||
# shift
|
|
||||||
|
|
||||||
# command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
|
||||||
|
|
||||||
# local -a args=("$@")
|
|
||||||
# command::_preprocess_flags args
|
|
||||||
|
|
||||||
# local fn
|
|
||||||
# fn=$(command::fn "$cmd" run)
|
|
||||||
# core::call_function "$fn" ${args[@]+"${args[@]}"}
|
|
||||||
# }
|
|
||||||
|
|
||||||
function command::run() {
|
function command::run() {
|
||||||
local cmd="$1"
|
local cmd="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
command::_reset_mixin_state
|
|
||||||
|
|
||||||
# Build default args from config
|
|
||||||
local -a default_args=()
|
|
||||||
local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
|
|
||||||
if [[ -n "$defaults" ]]; then
|
|
||||||
read -ra default_args <<< "$defaults"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local -a user_args=("$@")
|
|
||||||
[[ $# -gt 0 ]] && user_args=("$@")
|
|
||||||
|
|
||||||
# Resolve exclusive group conflicts — user args override defaults
|
|
||||||
local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
|
||||||
if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
|
|
||||||
command::_resolve_conflicts default_args user_args "$groups"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local -a cleaned_defaults=()
|
command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
||||||
for _d in "${default_args[@]:-}"; do
|
|
||||||
[[ -n "$_d" ]] && cleaned_defaults+=("$_d")
|
|
||||||
done
|
|
||||||
default_args=("${cleaned_defaults[@]:-}")
|
|
||||||
|
|
||||||
local -a args=()
|
local -a args=("$@")
|
||||||
for _d in "${default_args[@]:-}"; do
|
|
||||||
[[ -n "$_d" ]] && args+=("$_d")
|
|
||||||
done
|
|
||||||
for _u in "${user_args[@]:-}"; do
|
|
||||||
[[ -n "$_u" ]] && args+=("$_u")
|
|
||||||
done
|
|
||||||
|
|
||||||
# Preprocess mixin flags (--json, --no-color etc)
|
|
||||||
command::_preprocess_flags args
|
command::_preprocess_flags args
|
||||||
|
|
||||||
local fn
|
local fn
|
||||||
|
|
@ -122,9 +77,7 @@ function load_command() {
|
||||||
source "$path"
|
source "$path"
|
||||||
_LOADED_COMMANDS["$name"]=1
|
_LOADED_COMMANDS["$name"]=1
|
||||||
|
|
||||||
_CURRENT_LOADING_CMD="$name"
|
|
||||||
core::call_if_exists "$(command::fn "$name" on_load)"
|
core::call_if_exists "$(command::fn "$name" on_load)"
|
||||||
_CURRENT_LOADING_CMD=""
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# core/command_mixins.sh
|
# core/command_mixins.sh
|
||||||
# Mixin infrastructure — loads mixin files and provides command::mixin / flag::exclusive
|
# Mixin infrastructure — loads mixin files and provides command::mixin
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Active mixin tracking (per-process)
|
# Active mixin tracking (per-process)
|
||||||
|
|
@ -108,88 +108,4 @@ function command::_preprocess_flags() {
|
||||||
else
|
else
|
||||||
_args_ref=()
|
_args_ref=()
|
||||||
fi
|
fi
|
||||||
}
|
|
||||||
|
|
||||||
# command::_resolve_conflicts <defaults_nameref> <user_nameref> <groups_string>
|
|
||||||
# Removes conflicting defaults when user provides a member of an exclusive group
|
|
||||||
function command::_resolve_conflicts() {
|
|
||||||
local -n _def_ref="$1"
|
|
||||||
local -n _usr_ref="$2"
|
|
||||||
local groups="$3"
|
|
||||||
|
|
||||||
[[ -z "$groups" ]] && return 0
|
|
||||||
[[ ${#_def_ref[@]} -eq 0 ]] && return 0
|
|
||||||
|
|
||||||
# Work on a copy — progressively filter across all groups
|
|
||||||
local -a working=("${_def_ref[@]}")
|
|
||||||
|
|
||||||
local group
|
|
||||||
while IFS= read -r group; do
|
|
||||||
[[ -z "$group" ]] && continue
|
|
||||||
|
|
||||||
local -a members=()
|
|
||||||
IFS=',' read -ra members <<< "$group"
|
|
||||||
|
|
||||||
# Find which member (if any) the user passed from this group
|
|
||||||
local user_member=""
|
|
||||||
local member user_arg
|
|
||||||
for member in "${members[@]}"; do
|
|
||||||
for user_arg in "${_usr_ref[@]:-}"; do
|
|
||||||
if [[ "$user_arg" == "$member" ]]; then
|
|
||||||
user_member="$member"
|
|
||||||
break 2
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
# No user member in this group — don't touch defaults
|
|
||||||
[[ -z "$user_member" ]] && continue
|
|
||||||
|
|
||||||
# User passed a member — remove all OTHER members from defaults
|
|
||||||
# (keep the same flag if it was already in defaults)
|
|
||||||
local -a new_working=()
|
|
||||||
local def_arg
|
|
||||||
for def_arg in "${working[@]:-}"; do
|
|
||||||
local is_other_member=false
|
|
||||||
for member in "${members[@]}"; do
|
|
||||||
# It's another member if it's in the group AND not the same as user's choice
|
|
||||||
if [[ "$def_arg" == "$member" && "$def_arg" != "$user_member" ]]; then
|
|
||||||
is_other_member=true
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
$is_other_member || new_working+=("$def_arg")
|
|
||||||
done
|
|
||||||
working=("${new_working[@]:-}")
|
|
||||||
done < <(echo "$groups" | tr '|' '\n')
|
|
||||||
|
|
||||||
# Write back
|
|
||||||
if [[ ${#working[@]} -gt 0 ]]; then
|
|
||||||
_def_ref=("${working[@]}")
|
|
||||||
else
|
|
||||||
_def_ref=()
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Flag Exclusive
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
declare -gA _FLAG_EXCLUSIVE_GROUPS=()
|
|
||||||
|
|
||||||
# flag::exclusive <flag1> <flag2> ...
|
|
||||||
# Called from on_load — registers mutually exclusive flags for current command
|
|
||||||
function flag::exclusive() {
|
|
||||||
local cmd="${_CURRENT_LOADING_CMD:-}"
|
|
||||||
[[ -z "$cmd" ]] && return 0
|
|
||||||
|
|
||||||
# Join flags with comma as one group
|
|
||||||
local group
|
|
||||||
group=$(IFS=','; echo "$*")
|
|
||||||
|
|
||||||
if [[ -n "${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}" ]]; then
|
|
||||||
_FLAG_EXCLUSIVE_GROUPS["$cmd"]+="${group}|"
|
|
||||||
else
|
|
||||||
_FLAG_EXCLUSIVE_GROUPS["$cmd"]="${group}|"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
@ -44,50 +44,46 @@ _CTX_CONFIG_FILE="${_CTX_CONFIG}/wgctl.json"
|
||||||
# Accessors
|
# Accessors
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function ctx::root() { echo "$_CTX_ROOT"; }
|
function ctx::root() { echo "$_CTX_ROOT"; }
|
||||||
function ctx::core() { echo "$_CTX_CORE"; }
|
function ctx::core() { echo "$_CTX_CORE"; }
|
||||||
function ctx::modules() { echo "$_CTX_MODULES"; }
|
function ctx::modules() { echo "$_CTX_MODULES"; }
|
||||||
function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
||||||
function ctx::wg() { echo "$_CTX_WG"; }
|
function ctx::wg() { echo "$_CTX_WG"; }
|
||||||
function ctx::clients() { echo "$_CTX_CLIENTS"; }
|
function ctx::clients() { echo "$_CTX_CLIENTS"; }
|
||||||
|
|
||||||
# Top-level dirs
|
# Top-level dirs
|
||||||
function ctx::wgctl() { echo "$_CTX_WGCTL"; }
|
function ctx::wgctl() { echo "$_CTX_WGCTL"; }
|
||||||
function ctx::config() { echo "$_CTX_CONFIG"; }
|
function ctx::config() { echo "$_CTX_CONFIG"; }
|
||||||
function ctx::data() { echo "$_CTX_DATA"; }
|
function ctx::data() { echo "$_CTX_DATA"; }
|
||||||
function ctx::daemon() { echo "$_CTX_DAEMON"; }
|
function ctx::daemon() { echo "$_CTX_DAEMON"; }
|
||||||
|
|
||||||
# Data subdirs
|
# Data subdirs
|
||||||
function ctx::rules() { echo "$_CTX_RULES"; }
|
function ctx::rules() { echo "$_CTX_RULES"; }
|
||||||
function ctx::rules::base() { echo "$_CTX_RULES_BASE"; }
|
function ctx::rules::base() { echo "$_CTX_RULES_BASE"; }
|
||||||
function ctx::groups() { echo "$_CTX_GROUPS"; }
|
function ctx::groups() { echo "$_CTX_GROUPS"; }
|
||||||
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
||||||
function ctx::meta() { echo "$_CTX_META"; }
|
function ctx::meta() { echo "$_CTX_META"; }
|
||||||
function ctx::identities() { echo "$_CTX_IDENTITY"; }
|
function ctx::identities() { echo "$_CTX_IDENTITY"; }
|
||||||
function ctx::peer_history() { echo "$_CTX_PEER_HISTORY"; }
|
function ctx::peer_history() { echo "$_CTX_PEER_HISTORY"; }
|
||||||
|
|
||||||
# Data files
|
# Data files
|
||||||
function ctx::net() { echo "$_CTX_NET"; }
|
function ctx::net() { echo "$_CTX_NET"; }
|
||||||
function ctx::hosts() { echo "$_CTX_HOSTS"; }
|
function ctx::hosts() { echo "$_CTX_HOSTS"; }
|
||||||
function ctx::subnets() { echo "$_CTX_SUBNETS"; }
|
function ctx::subnets() { echo "$_CTX_SUBNETS"; }
|
||||||
function ctx::policies() { echo "$_CTX_POLICIES"; }
|
function ctx::policies() { echo "$_CTX_POLICIES"; }
|
||||||
|
|
||||||
# Config files
|
# Config files
|
||||||
function ctx::config_file() { echo "$_CTX_CONFIG_FILE"; }
|
function ctx::config_file() { echo "$_CTX_CONFIG_FILE"; }
|
||||||
function ctx::display() { echo "${_CTX_CONFIG}/display.json"; }
|
|
||||||
|
|
||||||
# Daemon files
|
# Daemon files
|
||||||
function ctx::events_log() { echo "${_CTX_DAEMON}/events.log"; }
|
function ctx::events_log() { echo "${_CTX_DAEMON}/events.log"; }
|
||||||
function ctx::fw_events_log() { echo "${_CTX_DAEMON}/fw_events.log"; }
|
function ctx::fw_events_log() { echo "${_CTX_DAEMON}/fw_events.log"; }
|
||||||
function ctx::endpoint_cache() { echo "${_CTX_DAEMON}/endpoint_cache.json"; }
|
function ctx::endpoint_cache() { echo "${_CTX_DAEMON}/endpoint_cache.json"; }
|
||||||
function ctx::accept_events_log() { echo "${_CTX_DAEMON}/accept_events.log"; }
|
|
||||||
|
|
||||||
# Tool paths
|
# Tool paths
|
||||||
function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
||||||
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
||||||
function ctx::lib() { echo "${_CTX_CORE}/lib"; }
|
function ctx::lib() { echo "${_CTX_CORE}/lib"; }
|
||||||
|
|
||||||
function ctx::block_history() { echo "${_CTX_DATA}/block-history"; }
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Path Helpers
|
# Path Helpers
|
||||||
|
|
|
||||||
18
core/json.sh
18
core/json.sh
|
|
@ -145,20 +145,9 @@ function json::error_envelope() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Config
|
# Config
|
||||||
|
|
||||||
function json::config_load() { python3 "$JSON_HELPER" config_load "$1" </dev/null; }
|
function json::config_load() { python3 "$JSON_HELPER" config_load "$1" </dev/null; }
|
||||||
|
|
||||||
function json::block_history_record() { python3 "$JSON_HELPER" block_history_record "$@" </dev/null; }
|
|
||||||
function json::block_history_unblock() { python3 "$JSON_HELPER" block_history_unblock "$@" </dev/null; }
|
|
||||||
function json::block_history_list() { python3 "$JSON_HELPER" block_history_list "$@" </dev/null; }
|
|
||||||
function json::block_history_list_all() { python3 "$JSON_HELPER" block_history_list_all "$@" </dev/null; }
|
|
||||||
|
|
||||||
function json::endpoint_cache_get() { python3 "$JSON_HELPER" endpoint_cache_get "$@" </dev/null; }
|
|
||||||
|
|
||||||
# Accept Events
|
|
||||||
function json::accept_events() { python3 "$JSON_HELPER" accept_events "$@" </dev/null; }
|
|
||||||
function json::accept_aggregate() { python3 "$JSON_HELPER" accept_aggregate "$@" </dev/null; }
|
|
||||||
function json::batch_resolve_dest() { python3 "$JSON_HELPER" batch_resolve_dest "$(ctx::net)" "$(ctx::hosts)" "$@" </dev/null; }
|
|
||||||
|
|
||||||
function json::peer_transfer() {
|
function json::peer_transfer() {
|
||||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||||
ACTIVITY_TOTAL_MED="$(config::activity_total_med)" \
|
ACTIVITY_TOTAL_MED="$(config::activity_total_med)" \
|
||||||
|
|
@ -172,8 +161,3 @@ function json::peer_transfer_delta() {
|
||||||
python3 "$JSON_HELPER" peer_transfer_delta "$@" </dev/null
|
python3 "$JSON_HELPER" peer_transfer_delta "$@" </dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# Importer
|
|
||||||
function json::import_peer() { python3 "$JSON_HELPER" import_peer "$@" </dev/null; }
|
|
||||||
function json::import_identity() { python3 "$JSON_HELPER" import_identity "$@" </dev/null; }
|
|
||||||
function json::import_full() { python3 "$JSON_HELPER" import_full "$@" </dev/null; }
|
|
||||||
function json::import_get_field() { python3 "$JSON_HELPER" import_get_field "$@" </dev/null; }
|
|
||||||
|
|
@ -1607,291 +1607,10 @@ def config_load(file):
|
||||||
emit('ACTIVITY_CURRENT_LOW_BYTES', acur.get('low'))
|
emit('ACTIVITY_CURRENT_LOW_BYTES', acur.get('low'))
|
||||||
emit('ACTIVITY_CURRENT_MED_BYTES', acur.get('medium'))
|
emit('ACTIVITY_CURRENT_MED_BYTES', acur.get('medium'))
|
||||||
emit('ACTIVITY_CURRENT_HIGH_BYTES', acur.get('high'))
|
emit('ACTIVITY_CURRENT_HIGH_BYTES', acur.get('high'))
|
||||||
|
|
||||||
# Command defaults and aliases
|
|
||||||
# Output format:
|
|
||||||
# CMD_DEFAULT:activity=--exclude-service pihole:dns-udp --limit 50
|
|
||||||
# CMD_ALIAS:act=activity
|
|
||||||
# CMD_ALIAS:a=activity
|
|
||||||
cmds = d.get('commands', {})
|
|
||||||
for cmd_name, cmd_cfg in cmds.items():
|
|
||||||
if not isinstance(cmd_cfg, dict):
|
|
||||||
continue
|
|
||||||
defaults = cmd_cfg.get('defaults', [])
|
|
||||||
if defaults:
|
|
||||||
print(f"CMD_DEFAULT:{cmd_name}={' '.join(str(x) for x in defaults)}")
|
|
||||||
aliases = cmd_cfg.get('aliases', [])
|
|
||||||
for alias in aliases:
|
|
||||||
print(f"CMD_ALIAS:{alias}={cmd_name}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def display_load(file):
|
|
||||||
"""
|
|
||||||
Load display.json and output view=style pairs.
|
|
||||||
Output: view_name=style per line
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
with open(file) as f:
|
|
||||||
d = json.load(f)
|
|
||||||
views = d.get('views', {})
|
|
||||||
for view_name, view_config in views.items():
|
|
||||||
style = view_config.get('style', 'compact')
|
|
||||||
print(f"{view_name}={style}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def _export_peer_data(name, clients_dir, meta_dir, identities_dir, groups_dir, blocks_dir):
|
|
||||||
"""Build peer data dict for export."""
|
|
||||||
import glob, base64, os
|
|
||||||
|
|
||||||
conf_path = os.path.join(clients_dir, f"{name}.conf")
|
|
||||||
if not os.path.exists(conf_path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
with open(conf_path, 'rb') as f:
|
|
||||||
conf_b64 = base64.b64encode(f.read()).decode()
|
|
||||||
|
|
||||||
# Public key
|
|
||||||
key_path = os.path.join(clients_dir, f"{name}_public.key")
|
|
||||||
public_key = open(key_path).read().strip() if os.path.exists(key_path) else ''
|
|
||||||
|
|
||||||
# IP from conf
|
|
||||||
ip = ''
|
|
||||||
with open(conf_path) as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith('Address'):
|
|
||||||
ip = line.split('=')[1].strip().split('/')[0]
|
|
||||||
break
|
|
||||||
|
|
||||||
# Meta
|
|
||||||
meta = {}
|
|
||||||
meta_path = os.path.join(meta_dir, f"{name}.meta")
|
|
||||||
if os.path.exists(meta_path):
|
|
||||||
try:
|
|
||||||
with open(meta_path) as f:
|
|
||||||
meta = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
direct_rule = meta.get('rule', '')
|
|
||||||
|
|
||||||
# Identity + type — scan identity files once for both
|
|
||||||
identity = ''
|
|
||||||
peer_type = meta.get('type', '')
|
|
||||||
for id_file in sorted(glob.glob(os.path.join(identities_dir, '*.identity'))):
|
|
||||||
try:
|
|
||||||
with open(id_file) as f:
|
|
||||||
id_data = json.load(f)
|
|
||||||
if name in id_data.get('peers', []):
|
|
||||||
identity = id_data.get('name', '')
|
|
||||||
if not peer_type:
|
|
||||||
peer_type = id_data.get('devices', {}).get(name, {}).get('type', '')
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Groups
|
|
||||||
peer_groups = []
|
|
||||||
for grp_file in sorted(glob.glob(os.path.join(groups_dir, '*.group'))):
|
|
||||||
try:
|
|
||||||
with open(grp_file) as f:
|
|
||||||
g = json.load(f)
|
|
||||||
if name in g.get('peers', []):
|
|
||||||
peer_groups.append(g.get('name', ''))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Blocks
|
|
||||||
blocks = {'is_blocked': False, 'block_file': None}
|
|
||||||
block_path = os.path.join(blocks_dir, f"{name}.block")
|
|
||||||
if os.path.exists(block_path):
|
|
||||||
with open(block_path, 'rb') as f:
|
|
||||||
blocks = {
|
|
||||||
'is_blocked': True,
|
|
||||||
'block_file': base64.b64encode(f.read()).decode()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'name': name,
|
|
||||||
'ip': ip,
|
|
||||||
'type': peer_type,
|
|
||||||
'public_key': public_key,
|
|
||||||
'conf': conf_b64,
|
|
||||||
'meta': meta,
|
|
||||||
'identity': identity,
|
|
||||||
'groups': peer_groups,
|
|
||||||
'direct_rule': direct_rule,
|
|
||||||
'blocks': blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def export_full(clients_dir, meta_dir, rules_dir, identities_dir,
|
|
||||||
groups_dir, blocks_dir, block_history_dir, config_file,
|
|
||||||
policies_file, subnets_file, net_file, hosts_file,
|
|
||||||
no_config, no_peers, version):
|
|
||||||
"""Build full wgctl export as JSON."""
|
|
||||||
import glob, os
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
# Config
|
|
||||||
if no_config != 'true' and os.path.exists(config_file):
|
|
||||||
try:
|
|
||||||
with open(config_file) as f:
|
|
||||||
data['config'] = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
data['config'] = {}
|
|
||||||
|
|
||||||
# Peers
|
|
||||||
if no_peers != 'true':
|
|
||||||
peers = []
|
|
||||||
for conf_path in sorted(glob.glob(f"{clients_dir}/*.conf")):
|
|
||||||
name = os.path.basename(conf_path).replace('.conf', '')
|
|
||||||
try:
|
|
||||||
peer = _export_peer_data(
|
|
||||||
name, clients_dir, meta_dir, identities_dir,
|
|
||||||
groups_dir, blocks_dir)
|
|
||||||
if peer:
|
|
||||||
peers.append(peer)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
data['peers'] = peers
|
|
||||||
|
|
||||||
# Rules
|
|
||||||
rules = []
|
|
||||||
for rule_file in sorted(
|
|
||||||
glob.glob(f"{rules_dir}/*.rule") +
|
|
||||||
glob.glob(f"{rules_dir}/base/*.rule")
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
with open(rule_file) as f:
|
|
||||||
rules.append(json.load(f))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
data['rules'] = rules
|
|
||||||
|
|
||||||
# Identities
|
|
||||||
identities = []
|
|
||||||
for id_file in sorted(glob.glob(f"{identities_dir}/*.identity")):
|
|
||||||
try:
|
|
||||||
with open(id_file) as f:
|
|
||||||
identities.append(json.load(f))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
data['identities'] = identities
|
|
||||||
|
|
||||||
# Groups
|
|
||||||
groups = []
|
|
||||||
for grp_file in sorted(glob.glob(f"{groups_dir}/*.group")):
|
|
||||||
try:
|
|
||||||
with open(grp_file) as f:
|
|
||||||
groups.append(json.load(f))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
data['groups'] = groups
|
|
||||||
|
|
||||||
|
|
||||||
# Block history
|
|
||||||
block_histories = []
|
|
||||||
for path in sorted(glob.glob(f"{block_history_dir}/*.json")):
|
|
||||||
try:
|
|
||||||
with open(path) as f:
|
|
||||||
block_histories.append(json.load(f))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
data['block_history'] = block_histories
|
|
||||||
|
|
||||||
# Flat JSON files
|
|
||||||
for key, path in [
|
|
||||||
('policies', policies_file),
|
|
||||||
('subnets', subnets_file),
|
|
||||||
('services', net_file),
|
|
||||||
('hosts', hosts_file),
|
|
||||||
]:
|
|
||||||
if os.path.exists(path):
|
|
||||||
try:
|
|
||||||
with open(path) as f:
|
|
||||||
data[key] = json.load(f)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
ts = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
result = {
|
|
||||||
'wgctl_version': version,
|
|
||||||
'export_type': 'full',
|
|
||||||
'exported_at': ts,
|
|
||||||
'data': data,
|
|
||||||
}
|
|
||||||
print(json.dumps(result))
|
|
||||||
|
|
||||||
def endpoint_cache_get(cache_file, peer):
|
|
||||||
"""Get cached endpoint IP for a peer."""
|
|
||||||
try:
|
|
||||||
with open(cache_file) as f:
|
|
||||||
cache = json.load(f)
|
|
||||||
print(cache.get(peer, ''))
|
|
||||||
except Exception:
|
|
||||||
print('')
|
|
||||||
|
|
||||||
def batch_resolve_dest(net_file, hosts_file, *dest_specs):
|
|
||||||
"""
|
|
||||||
Resolve multiple ip:port:proto specs at once.
|
|
||||||
Input: "ip:port:proto" strings
|
|
||||||
Output: "ip:port:proto|display_name" per line
|
|
||||||
Uses same logic as resolve::dest bash function.
|
|
||||||
"""
|
|
||||||
from lib.util import load_net_data, load_hosts_data, reverse_lookup, hosts_lookup
|
|
||||||
net_data = load_net_data(net_file)
|
|
||||||
hosts_data = load_hosts_data(hosts_file)
|
|
||||||
seen = set()
|
|
||||||
|
|
||||||
for spec in dest_specs:
|
|
||||||
if not spec or spec in seen:
|
|
||||||
continue
|
|
||||||
seen.add(spec)
|
|
||||||
|
|
||||||
parts = spec.split(':')
|
|
||||||
if len(parts) < 3:
|
|
||||||
print(f"{spec}|{spec}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
ip = parts[0]
|
|
||||||
port = parts[1]
|
|
||||||
proto = parts[2]
|
|
||||||
|
|
||||||
# Try service name first
|
|
||||||
svc = reverse_lookup(net_data, ip, port, proto)
|
|
||||||
if svc and svc != ip:
|
|
||||||
if port:
|
|
||||||
display = f"{svc}:{proto}-{port}" if False else f"{svc}"
|
|
||||||
# Use same format as resolve::dest: "svcname/proto" or "svcname:port"
|
|
||||||
display = svc
|
|
||||||
else:
|
|
||||||
display = svc
|
|
||||||
print(f"{spec}|{display}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Try host name
|
|
||||||
host = hosts_lookup(hosts_data, ip)
|
|
||||||
if host and host != ip:
|
|
||||||
if port:
|
|
||||||
print(f"{spec}|{host}:{port}/{proto}")
|
|
||||||
else:
|
|
||||||
print(f"{spec}|{host}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Raw fallback
|
|
||||||
if port:
|
|
||||||
print(f"{spec}|{ip}:{port}/{proto}")
|
|
||||||
else:
|
|
||||||
print(f"{spec}|{ip}")
|
|
||||||
|
|
||||||
|
|
||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
||||||
def _net_read(file):
|
def _net_read(file):
|
||||||
|
|
@ -2148,8 +1867,7 @@ commands = {
|
||||||
args[0], args[1], args[2], args[3], args[4],
|
args[0], args[1], args[2], args[3], args[4],
|
||||||
args[5], args[6] if len(args) > 6 else '24',
|
args[5], args[6] if len(args) > 6 else '24',
|
||||||
args[7] if len(args) > 7 else '',
|
args[7] if len(args) > 7 else '',
|
||||||
args[8] if len(args) > 8 else '',
|
args[8] if len(args) > 8 else ''),
|
||||||
args[9] if len(args) > 9 else ''),
|
|
||||||
# Rules
|
# Rules
|
||||||
'rule_resolve': lambda args: rule_resolve(args[0], args[1]),
|
'rule_resolve': lambda args: rule_resolve(args[0], args[1]),
|
||||||
'rule_resolve_field':lambda args: rule_resolve_field(args[0], args[1], args[2]),
|
'rule_resolve_field':lambda args: rule_resolve_field(args[0], args[1], args[2]),
|
||||||
|
|
@ -2263,55 +1981,6 @@ commands = {
|
||||||
'batch_resolve': lambda args: batch_resolve(args[0], args[1], *args[2:]),
|
'batch_resolve': lambda args: batch_resolve(args[0], args[1], *args[2:]),
|
||||||
'peer_history_lookup': lambda args: peer_history_lookup(args[0], args[1]),
|
'peer_history_lookup': lambda args: peer_history_lookup(args[0], args[1]),
|
||||||
'config_load': lambda args: config_load(args[0]),
|
'config_load': lambda args: config_load(args[0]),
|
||||||
'display_load': lambda args: display_load(args[0]),
|
|
||||||
'export_full': lambda args: export_full(
|
|
||||||
args[0], args[1], args[2], args[3], args[4], args[5],
|
|
||||||
args[6], args[7], args[8], args[9], args[10], args[11],
|
|
||||||
args[12] if len(args) > 12 else 'false',
|
|
||||||
args[13] if len(args) > 13 else 'false',
|
|
||||||
args[14] if len(args) > 14 else 'unknown'),
|
|
||||||
|
|
||||||
# Import
|
|
||||||
'import_peer': lambda args: __import__('lib.importer', fromlist=['import_peer']).import_peer(
|
|
||||||
args[0], args[1], args[2], args[3], args[4],
|
|
||||||
args[5], args[6], args[7] if len(args) > 7 else 'false'),
|
|
||||||
'import_identity': lambda args: __import__('lib.importer', fromlist=['import_identity']).import_identity(
|
|
||||||
args[0], args[1], args[2], args[3],
|
|
||||||
args[4] if len(args) > 4 else 'false'),
|
|
||||||
'import_full': lambda args: __import__('lib.importer', fromlist=['import_full']).import_full(
|
|
||||||
args[0], args[1], args[2], args[3], args[4], args[5],
|
|
||||||
args[6], args[7], args[8], args[9], args[10],
|
|
||||||
args[11] if len(args) > 11 else 'false'),
|
|
||||||
'import_get_field': lambda args: __import__('lib.importer', fromlist=['import_get_field']).import_get_field(args[0], *args[1:]),
|
|
||||||
'block_history_record': lambda args: __import__('lib.block_history',
|
|
||||||
fromlist=['block_history_record']).block_history_record(
|
|
||||||
args[0], args[1], args[2], args[3],
|
|
||||||
args[4] if len(args) > 4 else '',
|
|
||||||
args[5] if len(args) > 5 else ''),
|
|
||||||
'block_history_unblock': lambda args: __import__('lib.block_history',
|
|
||||||
fromlist=['block_history_unblock']).block_history_unblock(
|
|
||||||
args[0], args[1], args[2],
|
|
||||||
args[3] if len(args) > 3 else ''),
|
|
||||||
'block_history_list': lambda args: __import__('lib.block_history',
|
|
||||||
fromlist=['block_history_list']).block_history_list(args[0], args[1]),
|
|
||||||
'block_history_list_all': lambda args: __import__('lib.block_history',
|
|
||||||
fromlist=['block_history_list_all']).block_history_list_all(args[0]),
|
|
||||||
'endpoint_cache_get': lambda args: endpoint_cache_get(args[0], args[1]),
|
|
||||||
'accept_events': lambda args: __import__('lib.accept_events', fromlist=['accept_events']).accept_events(
|
|
||||||
args[0], args[1], args[2], args[3],
|
|
||||||
args[4] if len(args) > 4 else '100',
|
|
||||||
args[5] if len(args) > 5 else '1',
|
|
||||||
args[6] if len(args) > 6 else '',
|
|
||||||
args[7] if len(args) > 7 else '0',
|
|
||||||
args[8] if len(args) > 8 else 'desc'),
|
|
||||||
'accept_aggregate': lambda args: __import__('lib.accept_events',
|
|
||||||
fromlist=['accept_aggregate']).accept_aggregate(
|
|
||||||
args[0], args[1], args[2],
|
|
||||||
args[3] if len(args) > 3 else '',
|
|
||||||
args[4] if len(args) > 4 else '',
|
|
||||||
args[5] if len(args) > 5 else '0',
|
|
||||||
args[6] if len(args) > 6 else ''),
|
|
||||||
'batch_resolve_dest': lambda args: batch_resolve_dest(args[0], args[1], *args[2:]),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,260 +0,0 @@
|
||||||
"""
|
|
||||||
accept_events.py — conntrack accept event processing.
|
|
||||||
|
|
||||||
Reads accept_events.log written by wgctl-conntrack daemon.
|
|
||||||
Each line is a JSON object with fields:
|
|
||||||
ts, peer, src_ip, dst_ip, dst_port, proto,
|
|
||||||
bytes_orig, bytes_reply, packets_orig, packets_reply,
|
|
||||||
duration_sec, service, event, external
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from lib.util import (
|
|
||||||
DATETIME_FMT,
|
|
||||||
load_net_data, load_hosts_data,
|
|
||||||
reverse_lookup, hosts_lookup,
|
|
||||||
fmt_ts, fmt_ts_hour, ts_to_unix, parse_since,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def accept_events(file, filter_peer, filter_type, net_file,
|
|
||||||
limit, collapse='1', since='', filter_external='0',
|
|
||||||
sort_order='desc'):
|
|
||||||
"""
|
|
||||||
Format accept events with optional aggregation.
|
|
||||||
|
|
||||||
Output per line (collapse=1):
|
|
||||||
ts|peer|dst_ip|dst_port|proto|bytes_total|packets_total|count|duration_avg
|
|
||||||
|
|
||||||
Output per line (collapse=0):
|
|
||||||
ts|peer|dst_ip|dst_port|proto|bytes_orig|bytes_reply|packets_orig|packets_reply|duration_sec
|
|
||||||
"""
|
|
||||||
do_collapse = str(collapse) != '0'
|
|
||||||
external_only = str(filter_external) == '1'
|
|
||||||
limit = int(limit) if limit else 100
|
|
||||||
since_dt = parse_since(since) if since else None
|
|
||||||
descending = sort_order != 'asc'
|
|
||||||
|
|
||||||
events = []
|
|
||||||
try:
|
|
||||||
with open(file) as f:
|
|
||||||
for line in f:
|
|
||||||
try:
|
|
||||||
e = json.loads(line.strip())
|
|
||||||
if not e.get('peer'):
|
|
||||||
continue
|
|
||||||
if filter_peer and e.get('peer') != filter_peer:
|
|
||||||
continue
|
|
||||||
if filter_type and not e.get('peer', '').startswith(filter_type + '-'):
|
|
||||||
continue
|
|
||||||
if external_only and not e.get('external', False):
|
|
||||||
continue
|
|
||||||
if not external_only and e.get('external', False):
|
|
||||||
continue
|
|
||||||
if since_dt:
|
|
||||||
ts_str = e.get('ts', '')
|
|
||||||
try:
|
|
||||||
from datetime import timezone
|
|
||||||
ev_dt = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
|
|
||||||
if ev_dt < since_dt:
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
events.append(e)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if do_collapse:
|
|
||||||
# Aggregate by peer + dst_ip + dst_port + proto + hour
|
|
||||||
buckets = defaultdict(lambda: {'count': 0, 'bytes': 0, 'packets': 0, 'duration': 0.0})
|
|
||||||
bucket_ts = {}
|
|
||||||
|
|
||||||
for e in events:
|
|
||||||
ts_str = e.get('ts', '')
|
|
||||||
peer = e.get('peer', '')
|
|
||||||
dst_ip = e.get('dst_ip', '')
|
|
||||||
dst_port = str(e.get('dst_port', ''))
|
|
||||||
proto = e.get('proto', '')
|
|
||||||
try:
|
|
||||||
dt = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
|
|
||||||
hour_key = (peer, dst_ip, dst_port, proto, dt.strftime('%Y-%m-%d %H'))
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
b = buckets[hour_key]
|
|
||||||
b['count'] += 1
|
|
||||||
b['bytes'] += e.get('bytes_orig', 0) + e.get('bytes_reply', 0)
|
|
||||||
b['packets'] += e.get('packets_orig', 0) + e.get('packets_reply', 0)
|
|
||||||
b['duration'] += e.get('duration_sec', 0.0)
|
|
||||||
|
|
||||||
if hour_key not in bucket_ts:
|
|
||||||
bucket_ts[hour_key] = dt
|
|
||||||
|
|
||||||
# Sort and limit
|
|
||||||
sorted_buckets = sorted(bucket_ts.items(), key=lambda x: x[1])
|
|
||||||
output = sorted_buckets[-limit:]
|
|
||||||
if descending:
|
|
||||||
output = list(reversed(output))
|
|
||||||
|
|
||||||
for hour_key, dt in output:
|
|
||||||
peer, dst_ip, dst_port, proto, _ = hour_key
|
|
||||||
b = buckets[hour_key]
|
|
||||||
ts_fmt = fmt_ts_hour(dt.isoformat())
|
|
||||||
dur_avg = b['duration'] / b['count'] if b['count'] > 0 else 0.0
|
|
||||||
print(f"{ts_fmt}|{peer}|{dst_ip}|{dst_port}|{proto}|{b['bytes']}|{b['packets']}|{b['count']}|{dur_avg:.1f}")
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Detailed — one row per event
|
|
||||||
result = [(ts_to_unix(e.get('ts', '')), e) for e in events]
|
|
||||||
result = result[-limit:]
|
|
||||||
if descending:
|
|
||||||
result.reverse()
|
|
||||||
|
|
||||||
for _, e in result:
|
|
||||||
ts_fmt = fmt_ts(e.get('ts', ''))
|
|
||||||
peer = e.get('peer', '')
|
|
||||||
dst_ip = e.get('dst_ip', '')
|
|
||||||
dst_port = str(e.get('dst_port', ''))
|
|
||||||
proto = e.get('proto', '')
|
|
||||||
b_orig = e.get('bytes_orig', 0)
|
|
||||||
b_reply = e.get('bytes_reply', 0)
|
|
||||||
p_orig = e.get('packets_orig', 0)
|
|
||||||
p_reply = e.get('packets_reply', 0)
|
|
||||||
dur = e.get('duration_sec', 0.0)
|
|
||||||
print(f"{ts_fmt}|{peer}|{dst_ip}|{dst_port}|{proto}|{b_orig}|{b_reply}|{p_orig}|{p_reply}|{dur:.1f}")
|
|
||||||
|
|
||||||
|
|
||||||
def accept_aggregate(file, net_file, clients_dir, since='',
|
|
||||||
filter_peer='', external_only='0', exclude_services=''):
|
|
||||||
"""
|
|
||||||
Aggregate accept events per peer — total bytes, packets, top destinations.
|
|
||||||
Used by wgctl activity to show accepted traffic alongside drops.
|
|
||||||
|
|
||||||
external_only='1': only show traffic to external IPs (non-private)
|
|
||||||
external_only='0': only show traffic to internal IPs (default)
|
|
||||||
|
|
||||||
Output:
|
|
||||||
peer|peer_name|bytes_in|bytes_out|packets_in|packets_out|conn_count
|
|
||||||
dest|peer_name|dst_ip|dst_port|proto|bytes_total|conn_count
|
|
||||||
"""
|
|
||||||
from collections import defaultdict
|
|
||||||
from itertools import groupby
|
|
||||||
from lib.util import load_net_data, hosts_lookup, reverse_lookup
|
|
||||||
|
|
||||||
since_dt = parse_since(since) if since else None
|
|
||||||
show_external = str(external_only) == '1'
|
|
||||||
|
|
||||||
peer_stats = defaultdict(lambda: {
|
|
||||||
'bytes_in': 0, 'bytes_out': 0,
|
|
||||||
'packets_in': 0, 'packets_out': 0,
|
|
||||||
'conn_count': 0
|
|
||||||
})
|
|
||||||
# dest_stats = defaultdict(lambda: {'bytes': 0, 'count': 0})
|
|
||||||
dest_stats = defaultdict(lambda: {'bytes_orig': 0, 'bytes_reply': 0, 'count': 0})
|
|
||||||
|
|
||||||
# Build exclusion set — supports service names and ip:port:proto
|
|
||||||
exclude_set = set()
|
|
||||||
if exclude_services:
|
|
||||||
for svc in exclude_services.split():
|
|
||||||
exclude_set.add(svc.strip())
|
|
||||||
|
|
||||||
net_data = load_net_data(net_file) if (net_file and exclude_set) else {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(file) as f:
|
|
||||||
for line in f:
|
|
||||||
try:
|
|
||||||
e = json.loads(line.strip())
|
|
||||||
peer = e.get('peer', '')
|
|
||||||
if not peer:
|
|
||||||
continue
|
|
||||||
if filter_peer and peer != filter_peer:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Filter by external/internal
|
|
||||||
is_external = e.get('external', False)
|
|
||||||
if show_external and not is_external:
|
|
||||||
continue
|
|
||||||
if not show_external and is_external:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if since_dt:
|
|
||||||
ts_str = e.get('ts', '')
|
|
||||||
try:
|
|
||||||
from datetime import timezone
|
|
||||||
ev_dt = datetime.fromisoformat(
|
|
||||||
ts_str.replace('Z', '+00:00'))
|
|
||||||
if ev_dt < since_dt:
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
dst_ip = e.get('dst_ip', '')
|
|
||||||
dst_port = str(e.get('dst_port', ''))
|
|
||||||
proto = e.get('proto', '')
|
|
||||||
b_orig = e.get('bytes_orig', 0)
|
|
||||||
b_reply = e.get('bytes_reply', 0)
|
|
||||||
p_orig = e.get('packets_orig', 0)
|
|
||||||
p_reply = e.get('packets_reply', 0)
|
|
||||||
|
|
||||||
ps = peer_stats[peer]
|
|
||||||
ps['bytes_out'] += b_orig
|
|
||||||
ps['bytes_in'] += b_reply
|
|
||||||
ps['packets_out'] += p_orig
|
|
||||||
ps['packets_in'] += p_reply
|
|
||||||
ps['conn_count'] += 1
|
|
||||||
|
|
||||||
if _is_excluded(dst_ip, dst_port, proto, exclude_set, net_data):
|
|
||||||
continue
|
|
||||||
|
|
||||||
dest_key = (peer, dst_ip, dst_port, proto)
|
|
||||||
dest_stats[dest_key]['bytes_orig'] += b_orig
|
|
||||||
dest_stats[dest_key]['bytes_reply'] += b_reply
|
|
||||||
dest_stats[dest_key]['count'] += 1
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Output peer summaries
|
|
||||||
for peer, ps in sorted(peer_stats.items()):
|
|
||||||
print(f"peer|{peer}|{ps['bytes_in']}|{ps['bytes_out']}|"
|
|
||||||
f"{ps['packets_in']}|{ps['packets_out']}|{ps['conn_count']}")
|
|
||||||
|
|
||||||
# Output top 5 destinations per peer sorted by byte count
|
|
||||||
dest_items = sorted(
|
|
||||||
dest_stats.items(),
|
|
||||||
key=lambda x: (x[0][0], -(x[1]['bytes_orig'] + x[1]['bytes_reply']))
|
|
||||||
)
|
|
||||||
for peer, group in groupby(dest_items, key=lambda x: x[0][0]):
|
|
||||||
top = list(group)[:20]
|
|
||||||
for (p, dst_ip, dst_port, proto), stats in top:
|
|
||||||
print(f"dest|{p}|{dst_ip}|{dst_port}|{proto}|"
|
|
||||||
f"{stats['bytes_orig']}|{stats['bytes_reply']}|{stats['count']}")
|
|
||||||
|
|
||||||
|
|
||||||
def _is_excluded(ip, port, proto, exclude_set, net_data):
|
|
||||||
if not exclude_set:
|
|
||||||
return False
|
|
||||||
# Check raw ip:port:proto
|
|
||||||
if f"{ip}:{port}:{proto}" in exclude_set:
|
|
||||||
return True
|
|
||||||
# Check service name
|
|
||||||
svc = reverse_lookup(net_data, ip, str(port), proto) if net_data else ''
|
|
||||||
if svc and svc in exclude_set:
|
|
||||||
return True
|
|
||||||
# Check service:proto format (e.g. "pihole:dns-udp" -> "pihole" + "dns-udp")
|
|
||||||
if svc:
|
|
||||||
for excl in exclude_set:
|
|
||||||
if ':' in excl:
|
|
||||||
excl_svc, excl_port = excl.rsplit(':', 1)
|
|
||||||
if excl_svc == svc and excl_port in (f"{proto}-{port}", f"dns-{proto}"):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
@ -19,49 +19,26 @@ from lib.util import (
|
||||||
|
|
||||||
def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||||
clients_dir, meta_dir, hours, filter_peer,
|
clients_dir, meta_dir, hours, filter_peer,
|
||||||
filter_service_ip, exclude_services=''):
|
filter_service_ip):
|
||||||
"""
|
"""
|
||||||
Aggregate activity data for wgctl activity.
|
Aggregate activity data for wgctl activity.
|
||||||
Output:
|
Output:
|
||||||
peer|name|rx_bytes|tx_bytes|drop_count
|
peer|name|rx_bytes|tx_bytes|drop_count
|
||||||
service|peer_name|dest_display|dst_ip|dst_port|proto|drop_count
|
service|peer_name|dest_display|drop_count
|
||||||
"""
|
"""
|
||||||
hours = int(hours) if hours else 24
|
hours = int(hours) if hours else 24
|
||||||
cutoff = None
|
cutoff = None
|
||||||
if hours > 0:
|
if hours > 0:
|
||||||
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
|
||||||
|
|
||||||
# Build exclusion set
|
|
||||||
exclude_set = set()
|
|
||||||
if exclude_services:
|
|
||||||
for svc in exclude_services.split():
|
|
||||||
exclude_set.add(svc.strip())
|
|
||||||
|
|
||||||
# Preload lookups once
|
# Preload lookups once
|
||||||
ip_to_peer = build_ip_to_name(clients_dir)
|
ip_to_peer = build_ip_to_name(clients_dir)
|
||||||
pubkey_to_peer = build_pubkey_to_name(clients_dir)
|
pubkey_to_peer = build_pubkey_to_name(clients_dir)
|
||||||
net_data = load_net_data(net_file)
|
net_data = load_net_data(net_file)
|
||||||
|
|
||||||
def _reverse(dest_ip, dest_port, proto):
|
def _reverse(dest_ip, dest_port, proto):
|
||||||
return reverse_lookup(net_data, dest_ip, dest_port, proto)
|
return reverse_lookup(net_data, dest_ip, dest_port, proto)
|
||||||
|
|
||||||
def _is_excluded(ip, port, proto, svc_name):
|
|
||||||
if not exclude_set:
|
|
||||||
return False
|
|
||||||
if f"{ip}:{port}:{proto}" in exclude_set:
|
|
||||||
return True
|
|
||||||
if svc_name and svc_name in exclude_set:
|
|
||||||
return True
|
|
||||||
if svc_name:
|
|
||||||
for excl in exclude_set:
|
|
||||||
if ':' in excl:
|
|
||||||
excl_svc, excl_port = excl.rsplit(':', 1)
|
|
||||||
if excl_svc == svc_name and excl_port in (
|
|
||||||
f"{proto}-{port}", f"dns-{proto}", f"dns-udp", f"dns-tcp"
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# WireGuard transfer totals
|
# WireGuard transfer totals
|
||||||
peer_rx = defaultdict(int)
|
peer_rx = defaultdict(int)
|
||||||
peer_tx = defaultdict(int)
|
peer_tx = defaultdict(int)
|
||||||
|
|
@ -80,12 +57,11 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||||
peer_tx[peer] += tx
|
peer_tx[peer] += tx
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Parse fw_events for drops
|
# Parse fw_events for drops
|
||||||
# service_drops[peer][(dest_display, dst_ip, dst_port, proto)] = count
|
|
||||||
peer_drops = defaultdict(int)
|
peer_drops = defaultdict(int)
|
||||||
service_drops = defaultdict(lambda: defaultdict(int))
|
service_drops = defaultdict(lambda: defaultdict(int))
|
||||||
|
|
||||||
if os.path.exists(fw_file):
|
if os.path.exists(fw_file):
|
||||||
try:
|
try:
|
||||||
with open(fw_file) as f:
|
with open(fw_file) as f:
|
||||||
|
|
@ -105,16 +81,16 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
src_ip = ev.get('src_ip', '')
|
src_ip = ev.get('src_ip', '')
|
||||||
if not src_ip:
|
if not src_ip:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
dest_ip = ev.get('dest_ip', '')
|
dest_ip = ev.get('dest_ip', '')
|
||||||
dest_port = str(ev.get('dest_port', ''))
|
dest_port = str(ev.get('dest_port', ''))
|
||||||
proto_num = ev.get('ip.protocol', 0)
|
proto_num = ev.get('ip.protocol', 0)
|
||||||
proto = PROTO_MAP.get(int(proto_num), str(proto_num))
|
proto = PROTO_MAP.get(int(proto_num), str(proto_num))
|
||||||
|
|
||||||
peer = ip_to_peer.get(src_ip)
|
peer = ip_to_peer.get(src_ip)
|
||||||
if not peer:
|
if not peer:
|
||||||
continue
|
continue
|
||||||
|
|
@ -122,23 +98,18 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||||
continue
|
continue
|
||||||
if filter_service_ip and dest_ip != filter_service_ip:
|
if filter_service_ip and dest_ip != filter_service_ip:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
svc_name = _reverse(dest_ip, dest_port, proto)
|
svc_name = _reverse(dest_ip, dest_port, proto)
|
||||||
dest_display = make_dest_display(dest_ip, dest_port, proto, svc_name)
|
dest_display = make_dest_display(dest_ip, dest_port, proto, svc_name)
|
||||||
|
|
||||||
if _is_excluded(dest_ip, dest_port, proto, svc_name):
|
|
||||||
continue
|
|
||||||
|
|
||||||
peer_drops[peer] += 1
|
peer_drops[peer] += 1
|
||||||
# Key includes raw ip:port:proto for --ports support
|
service_drops[peer][dest_display] += 1
|
||||||
svc_key = (dest_display, dest_ip, dest_port, proto)
|
|
||||||
service_drops[peer][svc_key] += 1
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Collect peers with any activity
|
# Collect peers with any activity
|
||||||
all_peers = set()
|
all_peers = set()
|
||||||
all_peers.update(k for k in peer_rx if peer_rx[k] > 0)
|
all_peers.update(k for k in peer_rx if peer_rx[k] > 0)
|
||||||
|
|
@ -146,14 +117,13 @@ def activity_aggregate(fw_file, wg_file, wg_interface, net_file,
|
||||||
all_peers.update(peer_drops.keys())
|
all_peers.update(peer_drops.keys())
|
||||||
if filter_peer:
|
if filter_peer:
|
||||||
all_peers = {p for p in all_peers if p == filter_peer}
|
all_peers = {p for p in all_peers if p == filter_peer}
|
||||||
|
|
||||||
for peer in sorted(all_peers):
|
for peer in sorted(all_peers):
|
||||||
rx = peer_rx.get(peer, 0)
|
rx = peer_rx.get(peer, 0)
|
||||||
tx = peer_tx.get(peer, 0)
|
tx = peer_tx.get(peer, 0)
|
||||||
drops = peer_drops.get(peer, 0)
|
drops = peer_drops.get(peer, 0)
|
||||||
print(f"peer|{peer}|{rx}|{tx}|{drops}")
|
print(f"peer|{peer}|{rx}|{tx}|{drops}")
|
||||||
|
|
||||||
svc_map = service_drops.get(peer, {})
|
svc_map = service_drops.get(peer, {})
|
||||||
for (dest_display, dst_ip, dst_port, proto), count in \
|
for dest_display, count in sorted(svc_map.items(), key=lambda x: -x[1]):
|
||||||
sorted(svc_map.items(), key=lambda x: -x[1]):
|
print(f"service|{peer}|{dest_display}|{count}")
|
||||||
print(f"service|{peer}|{dest_display}|{dst_ip}|{dst_port}|{proto}|{count}")
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
# core/lib/block_history.py
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
|
|
||||||
BLOCK_HISTORY_VERSION = 1
|
|
||||||
|
|
||||||
|
|
||||||
def _history_file(history_dir, peer):
|
|
||||||
return os.path.join(history_dir, f"{peer}.json")
|
|
||||||
|
|
||||||
|
|
||||||
def _load(history_dir, peer):
|
|
||||||
path = _history_file(history_dir, peer)
|
|
||||||
if os.path.exists(path):
|
|
||||||
try:
|
|
||||||
return json.load(open(path))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {"peer": peer, "version": BLOCK_HISTORY_VERSION, "history": []}
|
|
||||||
|
|
||||||
|
|
||||||
def _save(history_dir, peer, data):
|
|
||||||
os.makedirs(history_dir, exist_ok=True)
|
|
||||||
path = _history_file(history_dir, peer)
|
|
||||||
open(path, 'w').write(json.dumps(data, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
def _next_id(history):
|
|
||||||
if not history:
|
|
||||||
return 1
|
|
||||||
return max(int(e.get("id", 0)) for e in history) + 1
|
|
||||||
|
|
||||||
|
|
||||||
def _now():
|
|
||||||
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
|
|
||||||
|
|
||||||
def _get_endpoint(clients_dir, peer):
|
|
||||||
"""Try to get current endpoint from endpoint cache."""
|
|
||||||
cache_file = os.path.join(
|
|
||||||
os.path.dirname(os.path.dirname(clients_dir)),
|
|
||||||
'.wgctl', 'daemon', 'endpoint_cache.json')
|
|
||||||
try:
|
|
||||||
cache = json.load(open(cache_file))
|
|
||||||
return cache.get(peer, '')
|
|
||||||
except Exception:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
def block_history_record(history_dir, peer, block_type,
|
|
||||||
triggered_by, reason, endpoint_at_block):
|
|
||||||
"""Record a new block event."""
|
|
||||||
data = _load(history_dir, peer)
|
|
||||||
entry = {
|
|
||||||
"id": _next_id(data["history"]),
|
|
||||||
"blocked_at": _now(),
|
|
||||||
"unblocked_at": None,
|
|
||||||
"block_type": block_type,
|
|
||||||
"triggered_by": triggered_by,
|
|
||||||
"reason": reason or '',
|
|
||||||
"endpoint_at_block": endpoint_at_block or '',
|
|
||||||
"unblocked_by": None,
|
|
||||||
"unblock_reason": None,
|
|
||||||
}
|
|
||||||
data["history"].append(entry)
|
|
||||||
_save(history_dir, peer, data)
|
|
||||||
print(entry["id"])
|
|
||||||
|
|
||||||
|
|
||||||
def block_history_unblock(history_dir, peer, unblocked_by, unblock_reason):
|
|
||||||
"""Update the most recent open block event with unblock timestamp."""
|
|
||||||
data = _load(history_dir, peer)
|
|
||||||
# Find most recent entry without unblocked_at
|
|
||||||
for entry in reversed(data["history"]):
|
|
||||||
if entry.get("unblocked_at") is None:
|
|
||||||
entry["unblocked_at"] = _now()
|
|
||||||
entry["unblocked_by"] = unblocked_by
|
|
||||||
entry["unblock_reason"] = unblock_reason or ''
|
|
||||||
_save(history_dir, peer, data)
|
|
||||||
print(entry["id"])
|
|
||||||
return
|
|
||||||
# No open block found — not an error, peer may have been unblocked externally
|
|
||||||
|
|
||||||
|
|
||||||
def block_history_list(history_dir, peer):
|
|
||||||
"""Output block history for a peer as JSON."""
|
|
||||||
data = _load(history_dir, peer)
|
|
||||||
print(json.dumps(data))
|
|
||||||
|
|
||||||
|
|
||||||
def block_history_list_all(history_dir):
|
|
||||||
"""Output block history for all peers as JSON array."""
|
|
||||||
import glob
|
|
||||||
results = []
|
|
||||||
for path in sorted(glob.glob(os.path.join(history_dir, '*.json'))):
|
|
||||||
try:
|
|
||||||
results.append(json.load(open(path)))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
print(json.dumps(results))
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import glob
|
|
||||||
|
|
||||||
def import_peer(file, data_key, name, clients_dir, meta_dir,
|
|
||||||
groups_dir, blocks_dir, force):
|
|
||||||
"""
|
|
||||||
Import a single peer from an export bundle.
|
|
||||||
data_key: 'data' for peer export, 'peers' for full backup
|
|
||||||
Returns: list of imported items as strings
|
|
||||||
"""
|
|
||||||
import base64, os
|
|
||||||
|
|
||||||
d = json.load(open(file))
|
|
||||||
|
|
||||||
if data_key == 'data':
|
|
||||||
peer = d['data']
|
|
||||||
else:
|
|
||||||
# Find peer in full backup peers array
|
|
||||||
peers = d['data'].get('peers', [])
|
|
||||||
peer = next((p for p in peers if p['name'] == name), None)
|
|
||||||
if not peer:
|
|
||||||
print(f"error: peer '{name}' not found in backup", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
imported = []
|
|
||||||
|
|
||||||
# conf
|
|
||||||
conf_path = os.path.join(clients_dir, f"{name}.conf")
|
|
||||||
if os.path.exists(conf_path) and force != 'true':
|
|
||||||
print(f"error: peer '{name}' already exists, use --force to overwrite",
|
|
||||||
file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
os.makedirs(clients_dir, exist_ok=True)
|
|
||||||
conf = base64.b64decode(peer['conf']).decode()
|
|
||||||
open(conf_path, 'w').write(conf)
|
|
||||||
imported.append('conf')
|
|
||||||
|
|
||||||
# meta
|
|
||||||
meta = peer.get('meta', {})
|
|
||||||
if meta:
|
|
||||||
os.makedirs(meta_dir, exist_ok=True)
|
|
||||||
open(os.path.join(meta_dir, f"{name}.meta"), 'w').write(
|
|
||||||
json.dumps(meta, indent=2))
|
|
||||||
imported.append('meta')
|
|
||||||
|
|
||||||
# groups
|
|
||||||
for grp in peer.get('groups', []):
|
|
||||||
grp_file = os.path.join(groups_dir, f"{grp}.group")
|
|
||||||
if os.path.exists(grp_file):
|
|
||||||
try:
|
|
||||||
g = json.load(open(grp_file))
|
|
||||||
if name not in g.get('peers', []):
|
|
||||||
g.setdefault('peers', []).append(name)
|
|
||||||
open(grp_file, 'w').write(json.dumps(g, indent=2))
|
|
||||||
imported.append(f"group:{grp}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# blocks
|
|
||||||
blocks = peer.get('blocks', {})
|
|
||||||
if blocks.get('is_blocked') and blocks.get('block_file'):
|
|
||||||
os.makedirs(blocks_dir, exist_ok=True)
|
|
||||||
block_data = base64.b64decode(blocks['block_file'])
|
|
||||||
open(os.path.join(blocks_dir, f"{name}.block"), 'wb').write(block_data)
|
|
||||||
imported.append('block')
|
|
||||||
|
|
||||||
print('\n'.join(imported))
|
|
||||||
|
|
||||||
|
|
||||||
def import_identity(file, name, identities_dir, clients_dir, force):
|
|
||||||
"""Import an identity from an export bundle."""
|
|
||||||
import os
|
|
||||||
|
|
||||||
d = json.load(open(file))
|
|
||||||
id_data = d['data'].get('identity', d['data'])
|
|
||||||
|
|
||||||
# Check all referenced peers exist
|
|
||||||
peers = id_data.get('peers', [])
|
|
||||||
missing = [p for p in peers
|
|
||||||
if not os.path.exists(os.path.join(clients_dir, f"{p}.conf"))]
|
|
||||||
if missing:
|
|
||||||
print(f"error: missing peers: {' '.join(missing)}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
id_file = os.path.join(identities_dir, f"{name}.identity")
|
|
||||||
if os.path.exists(id_file) and force != 'true':
|
|
||||||
print(f"error: identity '{name}' already exists, use --force to overwrite",
|
|
||||||
file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
os.makedirs(identities_dir, exist_ok=True)
|
|
||||||
open(id_file, 'w').write(json.dumps(id_data, indent=2))
|
|
||||||
print('identity')
|
|
||||||
|
|
||||||
|
|
||||||
def import_full(file, clients_dir, meta_dir, rules_dir, identities_dir,
|
|
||||||
groups_dir, blocks_dir, policies_file, subnets_file,
|
|
||||||
net_file, hosts_file, force):
|
|
||||||
"""Import a full backup bundle."""
|
|
||||||
import base64, os, glob
|
|
||||||
|
|
||||||
d = json.load(open(file))
|
|
||||||
data = d['data']
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# Peers
|
|
||||||
for peer in data.get('peers', []):
|
|
||||||
name = peer.get('name', '')
|
|
||||||
if not name:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
conf_path = os.path.join(clients_dir, f"{name}.conf")
|
|
||||||
if os.path.exists(conf_path) and force != 'true':
|
|
||||||
results.append(f"skip:{name}")
|
|
||||||
continue
|
|
||||||
os.makedirs(clients_dir, exist_ok=True)
|
|
||||||
conf = base64.b64decode(peer['conf']).decode()
|
|
||||||
open(conf_path, 'w').write(conf)
|
|
||||||
|
|
||||||
meta = peer.get('meta', {})
|
|
||||||
if meta:
|
|
||||||
os.makedirs(meta_dir, exist_ok=True)
|
|
||||||
open(os.path.join(meta_dir, f"{name}.meta"), 'w').write(
|
|
||||||
json.dumps(meta, indent=2))
|
|
||||||
|
|
||||||
blocks = peer.get('blocks', {})
|
|
||||||
if blocks.get('is_blocked') and blocks.get('block_file'):
|
|
||||||
os.makedirs(blocks_dir, exist_ok=True)
|
|
||||||
block_data = base64.b64decode(blocks['block_file'])
|
|
||||||
open(os.path.join(blocks_dir, f"{name}.block"), 'wb').write(block_data)
|
|
||||||
|
|
||||||
results.append(f"peer:{name}")
|
|
||||||
except Exception as e:
|
|
||||||
results.append(f"error:{name}:{e}")
|
|
||||||
|
|
||||||
# Rules
|
|
||||||
os.makedirs(rules_dir, exist_ok=True)
|
|
||||||
for rule in data.get('rules', []):
|
|
||||||
name = rule.get('name', '')
|
|
||||||
if name:
|
|
||||||
open(os.path.join(rules_dir, f"{name}.rule"), 'w').write(
|
|
||||||
json.dumps(rule, indent=2))
|
|
||||||
results.append('rules')
|
|
||||||
|
|
||||||
# Identities
|
|
||||||
os.makedirs(identities_dir, exist_ok=True)
|
|
||||||
for identity in data.get('identities', []):
|
|
||||||
name = identity.get('name', '')
|
|
||||||
if name:
|
|
||||||
open(os.path.join(identities_dir, f"{name}.identity"), 'w').write(
|
|
||||||
json.dumps(identity, indent=2))
|
|
||||||
results.append('identities')
|
|
||||||
|
|
||||||
# Groups
|
|
||||||
os.makedirs(groups_dir, exist_ok=True)
|
|
||||||
for grp in data.get('groups', []):
|
|
||||||
name = grp.get('name', '')
|
|
||||||
if name:
|
|
||||||
open(os.path.join(groups_dir, f"{name}.group"), 'w').write(
|
|
||||||
json.dumps(grp, indent=2))
|
|
||||||
results.append('groups')
|
|
||||||
|
|
||||||
# Block history
|
|
||||||
bh_dir = os.path.join(os.path.dirname(groups_dir), 'block-history')
|
|
||||||
os.makedirs(bh_dir, exist_ok=True)
|
|
||||||
for bh in data.get('block_history', []):
|
|
||||||
peer_name = bh.get('peer', '')
|
|
||||||
if peer_name:
|
|
||||||
open(os.path.join(bh_dir, f"{peer_name}.json"), 'w').write(
|
|
||||||
json.dumps(bh, indent=2))
|
|
||||||
if data.get('block_history'):
|
|
||||||
results.append('block_history')
|
|
||||||
|
|
||||||
# Flat JSON files
|
|
||||||
for key, path in [('policies', policies_file), ('subnets', subnets_file),
|
|
||||||
('services', net_file), ('hosts', hosts_file)]:
|
|
||||||
section = data.get(key)
|
|
||||||
if section is not None:
|
|
||||||
open(path, 'w').write(json.dumps(section, indent=2))
|
|
||||||
results.append(key)
|
|
||||||
|
|
||||||
print('\n'.join(results))
|
|
||||||
|
|
||||||
|
|
||||||
def import_get_field(file, *keys):
|
|
||||||
"""Get a field from export JSON. Keys are dot-separated path."""
|
|
||||||
d = json.load(open(file))
|
|
||||||
val = d
|
|
||||||
for k in keys:
|
|
||||||
val = val.get(k, '')
|
|
||||||
if not val:
|
|
||||||
break
|
|
||||||
print(val if val else '')
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"phone-fred": "176.223.61.130",
|
"phone-fred": "176.223.61.130",
|
||||||
"phone-helena": "148.69.46.73",
|
"phone-helena": "148.69.46.73",
|
||||||
"phone-nuno": "94.63.0.129",
|
"phone-nuno": "148.69.50.62",
|
||||||
"tablet-nuno": "148.69.202.5",
|
"tablet-nuno": "148.69.202.5",
|
||||||
"guest-zephyr": "86.120.152.74",
|
"guest-zephyr": "86.120.152.74",
|
||||||
"guest-zephyr-test": "94.63.0.129",
|
"guest-zephyr-test": "94.63.0.129",
|
||||||
"desktop-roboclean": "46.189.215.231",
|
"desktop-roboclean": "46.189.215.231",
|
||||||
"laptop-nuno": "94.63.0.129",
|
"laptop-nuno": "94.63.0.129",
|
||||||
"phone-luis": "176.223.61.15",
|
"phone-luis": "176.223.61.15",
|
||||||
"phone-helena-2": "148.69.193.234",
|
"phone-helena-2": "148.69.203.225",
|
||||||
"desktop-zephyr": "86.120.152.74"
|
"desktop-zephyr": "86.120.152.74"
|
||||||
}
|
}
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Flags holds CLI flags
|
|
||||||
type Flags struct {
|
|
||||||
WGDir string
|
|
||||||
Subnet string
|
|
||||||
LogFile string
|
|
||||||
Version bool
|
|
||||||
}
|
|
||||||
|
|
||||||
const Version = "0.1.0"
|
|
||||||
|
|
||||||
func Parse() *Flags {
|
|
||||||
f := &Flags{}
|
|
||||||
flag.StringVar(&f.WGDir, "wg-dir", "/etc/wireguard", "WireGuard base directory")
|
|
||||||
flag.StringVar(&f.Subnet, "subnet", "", "WireGuard subnet override")
|
|
||||||
flag.StringVar(&f.LogFile, "log-file", "", "Accept events log file override")
|
|
||||||
flag.BoolVar(&f.Version, "version", false, "Print version and exit")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if f.Version {
|
|
||||||
fmt.Println(Version)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config holds wgctl-conntrack runtime configuration
|
|
||||||
type Config struct {
|
|
||||||
WGSubnet string
|
|
||||||
DataDir string
|
|
||||||
ClientsDir string
|
|
||||||
AcceptLogFile string
|
|
||||||
ServicesFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
type wgctlJSON struct {
|
|
||||||
WireGuard struct {
|
|
||||||
Subnet string `json:"subnet"`
|
|
||||||
} `json:"wireguard"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load reads config from wgctl.json and applies defaults
|
|
||||||
func Load(wgDir string) (*Config, error) {
|
|
||||||
cfg := &Config{
|
|
||||||
WGSubnet: "10.1.0.0/16",
|
|
||||||
DataDir: wgDir + "/.wgctl/data",
|
|
||||||
ClientsDir: wgDir + "/clients",
|
|
||||||
AcceptLogFile: wgDir + "/.wgctl/daemon/accept_events.log",
|
|
||||||
ServicesFile: wgDir + "/.wgctl/data/services.json",
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonFile := wgDir + "/.wgctl/config/wgctl.json"
|
|
||||||
if data, err := os.ReadFile(jsonFile); err == nil {
|
|
||||||
var wj wgctlJSON
|
|
||||||
if json.Unmarshal(data, &wj) == nil && wj.WireGuard.Subnet != "" {
|
|
||||||
cfg.WGSubnet = wj.WireGuard.Subnet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package conntrack
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// EventType represents the type of traffic event
|
|
||||||
type EventType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
EventAccept EventType = "accept"
|
|
||||||
EventExternal EventType = "external"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TrafficEvent is the normalized event written to the log
|
|
||||||
type TrafficEvent struct {
|
|
||||||
Timestamp time.Time `json:"ts"`
|
|
||||||
Peer string `json:"peer"`
|
|
||||||
SrcIP string `json:"src_ip"`
|
|
||||||
DstIP string `json:"dst_ip"`
|
|
||||||
DstPort uint16 `json:"dst_port"`
|
|
||||||
Proto string `json:"proto"`
|
|
||||||
BytesOrig uint64 `json:"bytes_orig"`
|
|
||||||
BytesReply uint64 `json:"bytes_reply"`
|
|
||||||
PacketsOrig uint64 `json:"packets_orig"`
|
|
||||||
PacketsReply uint64 `json:"packets_reply"`
|
|
||||||
DurationSec float64 `json:"duration_sec"`
|
|
||||||
Service string `json:"service,omitempty"`
|
|
||||||
Event EventType `json:"event"`
|
|
||||||
External bool `json:"external"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package conntrack
|
|
||||||
|
|
||||||
import "net"
|
|
||||||
|
|
||||||
var privateRanges = []string{
|
|
||||||
"10.0.0.0/8",
|
|
||||||
"172.16.0.0/12",
|
|
||||||
"192.168.0.0/16",
|
|
||||||
}
|
|
||||||
|
|
||||||
var privateCIDRs []*net.IPNet
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
for _, cidr := range privateRanges {
|
|
||||||
_, ipnet, _ := net.ParseCIDR(cidr)
|
|
||||||
privateCIDRs = append(privateCIDRs, ipnet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsWGPeer(ip net.IP, wgSubnet *net.IPNet) bool {
|
|
||||||
return wgSubnet.Contains(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsExternal(ip net.IP) bool {
|
|
||||||
for _, cidr := range privateCIDRs {
|
|
||||||
if cidr.Contains(ip) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProtoName(proto uint8) string {
|
|
||||||
switch proto {
|
|
||||||
case 6:
|
|
||||||
return "tcp"
|
|
||||||
case 17:
|
|
||||||
return "udp"
|
|
||||||
case 1:
|
|
||||||
return "icmp"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
package conntrack
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
ct "github.com/ti-mo/conntrack"
|
|
||||||
"github.com/ti-mo/netfilter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Resolver maps IPs and ports to peer/service names
|
|
||||||
type Resolver interface {
|
|
||||||
PeerForIP(ip net.IP) string
|
|
||||||
ServiceForDst(ip net.IP, port uint16, proto string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscriber listens for conntrack DESTROY events
|
|
||||||
type Subscriber struct {
|
|
||||||
wgSubnet *net.IPNet
|
|
||||||
events chan<- TrafficEvent
|
|
||||||
resolver Resolver
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSubscriber(wgSubnet *net.IPNet, events chan<- TrafficEvent, resolver Resolver) *Subscriber {
|
|
||||||
return &Subscriber{wgSubnet: wgSubnet, events: events, resolver: resolver}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) Run() error {
|
|
||||||
conn, err := ct.Dial(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
evCh := make(chan ct.Event, 256)
|
|
||||||
|
|
||||||
errCh, err := conn.Listen(evCh, 1, []netfilter.NetlinkGroup{
|
|
||||||
netfilter.GroupCTDestroy,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("conntrack subscriber started")
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case ev := <-evCh:
|
|
||||||
s.processEvent(ev)
|
|
||||||
case err := <-errCh:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Subscriber) processEvent(ev ct.Event) {
|
|
||||||
flow := ev.Flow
|
|
||||||
if flow == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tuple := flow.TupleOrig
|
|
||||||
|
|
||||||
// Skip IPv6
|
|
||||||
if !tuple.IP.SourceAddress.Is4() || !tuple.IP.DestinationAddress.Is4() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
srcBytes := tuple.IP.SourceAddress.As4()
|
|
||||||
dstBytes := tuple.IP.DestinationAddress.As4()
|
|
||||||
srcIP := net.IP(srcBytes[:])
|
|
||||||
dstIP := net.IP(dstBytes[:])
|
|
||||||
|
|
||||||
// Only process WireGuard peer traffic
|
|
||||||
if !IsWGPeer(srcIP, s.wgSubnet) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
proto := ProtoName(tuple.Proto.Protocol)
|
|
||||||
dstPort := tuple.Proto.DestinationPort
|
|
||||||
external := IsExternal(dstIP)
|
|
||||||
|
|
||||||
peer := s.resolver.PeerForIP(srcIP)
|
|
||||||
if peer == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
service := s.resolver.ServiceForDst(dstIP, dstPort, proto)
|
|
||||||
|
|
||||||
var durationSec float64
|
|
||||||
if flow.Timestamp.Stop.After(flow.Timestamp.Start) {
|
|
||||||
durationSec = flow.Timestamp.Stop.Sub(flow.Timestamp.Start).Seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
eventType := EventAccept
|
|
||||||
if external {
|
|
||||||
eventType = EventExternal
|
|
||||||
}
|
|
||||||
|
|
||||||
s.events <- TrafficEvent{
|
|
||||||
Timestamp: time.Now().UTC(),
|
|
||||||
Peer: peer,
|
|
||||||
SrcIP: srcIP.String(),
|
|
||||||
DstIP: dstIP.String(),
|
|
||||||
DstPort: dstPort,
|
|
||||||
Proto: proto,
|
|
||||||
BytesOrig: flow.CountersOrig.Bytes,
|
|
||||||
BytesReply: flow.CountersReply.Bytes,
|
|
||||||
PacketsOrig: flow.CountersOrig.Packets,
|
|
||||||
PacketsReply: flow.CountersReply.Packets,
|
|
||||||
DurationSec: durationSec,
|
|
||||||
Service: service,
|
|
||||||
Event: eventType,
|
|
||||||
External: external,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
module git.krilio.net/nuno/wgctl-conntrack
|
|
||||||
|
|
||||||
go 1.23.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
|
||||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/ti-mo/conntrack v0.6.0 // indirect
|
|
||||||
github.com/ti-mo/netfilter v0.5.3 // indirect
|
|
||||||
golang.org/x/net v0.39.0 // indirect
|
|
||||||
golang.org/x/sync v0.14.0 // indirect
|
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
|
||||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
|
||||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
|
||||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
|
||||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
|
||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/ti-mo/conntrack v0.6.0 h1:laiW2+dzKyS2u0aVr6FeRQs+v7cj4t7q+twolL/ZkjQ=
|
|
||||||
github.com/ti-mo/conntrack v0.6.0/go.mod h1:4HZrFQQLOSuBzgQNid3H/wYyyp1kfGXUYxueXjIGibo=
|
|
||||||
github.com/ti-mo/netfilter v0.5.3 h1:ikzduvnaUMwre5bhbNwWOd6bjqLMVb33vv0XXbK0xGQ=
|
|
||||||
github.com/ti-mo/netfilter v0.5.3/go.mod h1:08SyBCg6hu1qyQk4s3DjjJKNrm3RTb32nm6AzyT972E=
|
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
Binary file not shown.
|
|
@ -1,71 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"git.krilio.net/nuno/wgctl-conntrack/cmd"
|
|
||||||
"git.krilio.net/nuno/wgctl-conntrack/config"
|
|
||||||
ctconn "git.krilio.net/nuno/wgctl-conntrack/conntrack"
|
|
||||||
"git.krilio.net/nuno/wgctl-conntrack/resolver"
|
|
||||||
"git.krilio.net/nuno/wgctl-conntrack/writer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flags := cmd.Parse()
|
|
||||||
|
|
||||||
cfg, err := config.Load(flags.WGDir)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("failed to load config: %v", err)
|
|
||||||
}
|
|
||||||
if flags.Subnet != "" {
|
|
||||||
cfg.WGSubnet = flags.Subnet
|
|
||||||
}
|
|
||||||
if flags.LogFile != "" {
|
|
||||||
cfg.AcceptLogFile = flags.LogFile
|
|
||||||
}
|
|
||||||
|
|
||||||
_, wgSubnet, err := net.ParseCIDR(cfg.WGSubnet)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("invalid WG subnet %q: %v", cfg.WGSubnet, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("wgctl-conntrack v%s starting (subnet: %s, log: %s)",
|
|
||||||
cmd.Version, cfg.WGSubnet, cfg.AcceptLogFile)
|
|
||||||
|
|
||||||
peerResolver := resolver.NewPeerResolver(flags.WGDir)
|
|
||||||
svcResolver := resolver.NewServiceResolver(cfg.ServicesFile)
|
|
||||||
|
|
||||||
res := &combinedResolver{peers: peerResolver, services: svcResolver}
|
|
||||||
events := make(chan ctconn.TrafficEvent, 512)
|
|
||||||
|
|
||||||
go writer.NewLogWriter(cfg.AcceptLogFile).Run(events)
|
|
||||||
|
|
||||||
sub := ctconn.NewSubscriber(wgSubnet, events, res)
|
|
||||||
go func() {
|
|
||||||
if err := sub.Run(); err != nil {
|
|
||||||
log.Fatalf("conntrack subscriber error: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-sig
|
|
||||||
log.Println("wgctl-conntrack shutting down")
|
|
||||||
}
|
|
||||||
|
|
||||||
type combinedResolver struct {
|
|
||||||
peers *resolver.PeerResolver
|
|
||||||
services *resolver.ServiceResolver
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *combinedResolver) PeerForIP(ip net.IP) string {
|
|
||||||
return r.peers.PeerForIP(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *combinedResolver) ServiceForDst(ip net.IP, port uint16, proto string) string {
|
|
||||||
return r.services.ServiceForDst(ip, port, proto)
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PeerResolver maps WireGuard peer IPs to peer names
|
|
||||||
type PeerResolver struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
ipToName map[string]string
|
|
||||||
wgDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPeerResolver(wgDir string) *PeerResolver {
|
|
||||||
r := &PeerResolver{wgDir: wgDir, ipToName: make(map[string]string)}
|
|
||||||
r.reload()
|
|
||||||
go r.watchReload()
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PeerResolver) PeerForIP(ip net.IP) string {
|
|
||||||
r.mu.RLock()
|
|
||||||
defer r.mu.RUnlock()
|
|
||||||
return r.ipToName[ip.String()]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PeerResolver) reload() {
|
|
||||||
newMap := make(map[string]string)
|
|
||||||
|
|
||||||
// WireGuard IPs from conf files (10.1.x.x → peer name)
|
|
||||||
clientsDir := r.wgDir + "/clients"
|
|
||||||
entries, err := os.ReadDir(clientsDir)
|
|
||||||
if err == nil {
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".conf") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := strings.TrimSuffix(entry.Name(), ".conf")
|
|
||||||
if ip := parseAddressFromConf(clientsDir + "/" + entry.Name()); ip != "" {
|
|
||||||
newMap[ip] = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// External IPs from endpoint index (external IP → peer name)
|
|
||||||
indexFile := r.wgDir + "/.wgctl/data/peer-history/endpoint_index.json"
|
|
||||||
if data, err := os.ReadFile(indexFile); err == nil {
|
|
||||||
var index map[string]string
|
|
||||||
if json.Unmarshal(data, &index) == nil {
|
|
||||||
for ip, peer := range index {
|
|
||||||
newMap[ip] = peer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.mu.Lock()
|
|
||||||
r.ipToName = newMap
|
|
||||||
r.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PeerResolver) watchReload() {
|
|
||||||
ticker := time.NewTicker(60 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for range ticker.C {
|
|
||||||
r.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAddressFromConf(path string) string {
|
|
||||||
data, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if strings.HasPrefix(line, "Address") {
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
ip := strings.TrimSpace(parts[1])
|
|
||||||
if idx := strings.Index(ip, "/"); idx != -1 {
|
|
||||||
ip = ip[:idx]
|
|
||||||
}
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceResolver maps IP:port:proto to service names
|
|
||||||
type ServiceResolver struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
portToSvc map[string]string
|
|
||||||
servicesFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServiceResolver(servicesFile string) *ServiceResolver {
|
|
||||||
r := &ServiceResolver{servicesFile: servicesFile, portToSvc: make(map[string]string)}
|
|
||||||
r.reload()
|
|
||||||
go r.watchReload()
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ServiceResolver) ServiceForDst(ip net.IP, port uint16, proto string) string {
|
|
||||||
r.mu.RLock()
|
|
||||||
defer r.mu.RUnlock()
|
|
||||||
|
|
||||||
// Try IP:port:proto first
|
|
||||||
if svc, ok := r.portToSvc[fmt.Sprintf("%s:%d:%s", ip.String(), port, proto)]; ok {
|
|
||||||
return svc
|
|
||||||
}
|
|
||||||
// Fall back to IP only
|
|
||||||
if svc, ok := r.portToSvc[ip.String()]; ok {
|
|
||||||
return svc
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ServiceResolver) reload() {
|
|
||||||
data, err := os.ReadFile(r.servicesFile)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var services map[string]interface{}
|
|
||||||
if json.Unmarshal(data, &services) != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newMap := make(map[string]string)
|
|
||||||
for name, svcRaw := range services {
|
|
||||||
svc, ok := svcRaw.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
hosts := map[string]bool{}
|
|
||||||
if hostsRaw, ok := svc["hosts"].(map[string]interface{}); ok {
|
|
||||||
for ip := range hostsRaw {
|
|
||||||
hosts[ip] = true
|
|
||||||
newMap[ip] = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if portsRaw, ok := svc["ports"].([]interface{}); ok {
|
|
||||||
for _, portRaw := range portsRaw {
|
|
||||||
port, ok := portRaw.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
portNum := fmt.Sprintf("%.0f", port["port"])
|
|
||||||
proto, _ := port["proto"].(string)
|
|
||||||
for ip := range hosts {
|
|
||||||
newMap[fmt.Sprintf("%s:%s:%s", ip, portNum, proto)] = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.mu.Lock()
|
|
||||||
r.portToSvc = newMap
|
|
||||||
r.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ServiceResolver) watchReload() {
|
|
||||||
ticker := time.NewTicker(60 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for range ticker.C {
|
|
||||||
r.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
|
@ -1,21 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=wgctl conntrack accept logging daemon
|
|
||||||
After=network.target wg-quick@wg0.service
|
|
||||||
Requires=wg-quick@wg0.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/etc/wireguard/wgctl/daemon/wgctl-conntrack/wgctl-conntrack \
|
|
||||||
--wg-dir /etc/wireguard
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=5s
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=wgctl-conntrack
|
|
||||||
|
|
||||||
# Needs CAP_NET_ADMIN for netlink conntrack
|
|
||||||
AmbientCapabilities=CAP_NET_ADMIN
|
|
||||||
CapabilityBoundingSet=CAP_NET_ADMIN
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package writer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.krilio.net/nuno/wgctl-conntrack/conntrack"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LogWriter writes TrafficEvents as JSON lines to a file
|
|
||||||
type LogWriter struct {
|
|
||||||
path string
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogWriter(path string) *LogWriter {
|
|
||||||
return &LogWriter{path: path}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *LogWriter) Write(ev conntrack.TrafficEvent) error {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
|
|
||||||
f, err := os.OpenFile(w.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
data, err := json.Marshal(ev)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = f.Write(append(data, '\n'))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *LogWriter) Run(events <-chan conntrack.TrafficEvent) {
|
|
||||||
for ev := range events {
|
|
||||||
if err := w.Write(ev); err != nil {
|
|
||||||
log.Printf("error writing event: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,9 +22,6 @@ declare -g _ACTIVITY_CURRENT_LOW_BYTES="${ACTIVITY_CURRENT_LOW_BYTES:-1000000}"
|
||||||
declare -g _ACTIVITY_CURRENT_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}"
|
declare -g _ACTIVITY_CURRENT_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}"
|
||||||
declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}"
|
declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}"
|
||||||
|
|
||||||
declare -gA _COMMAND_DEFAULTS=()
|
|
||||||
declare -gA _COMMAND_ALIASES=()
|
|
||||||
|
|
||||||
function config::_init_defaults() {
|
function config::_init_defaults() {
|
||||||
_WG_INTERFACE="wg0"
|
_WG_INTERFACE="wg0"
|
||||||
_WG_DNS="10.0.0.103"
|
_WG_DNS="10.0.0.103"
|
||||||
|
|
@ -92,14 +89,6 @@ function config::_load_json() {
|
||||||
ACTIVITY_CURRENT_LOW_BYTES) _ACTIVITY_CURRENT_LOW_BYTES="$value" ;;
|
ACTIVITY_CURRENT_LOW_BYTES) _ACTIVITY_CURRENT_LOW_BYTES="$value" ;;
|
||||||
ACTIVITY_CURRENT_MED_BYTES) _ACTIVITY_CURRENT_MED_BYTES="$value" ;;
|
ACTIVITY_CURRENT_MED_BYTES) _ACTIVITY_CURRENT_MED_BYTES="$value" ;;
|
||||||
ACTIVITY_CURRENT_HIGH_BYTES) _ACTIVITY_CURRENT_HIGH_BYTES="$value" ;;
|
ACTIVITY_CURRENT_HIGH_BYTES) _ACTIVITY_CURRENT_HIGH_BYTES="$value" ;;
|
||||||
CMD_DEFAULT:*)
|
|
||||||
local cmd_name="${key#CMD_DEFAULT:}"
|
|
||||||
_COMMAND_DEFAULTS["$cmd_name"]="$value"
|
|
||||||
;;
|
|
||||||
CMD_ALIAS:*)
|
|
||||||
local alias_name="${key#CMD_ALIAS:}"
|
|
||||||
_COMMAND_ALIASES["$alias_name"]="$value"
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done < <(json::config_load "$file" 2>/dev/null)
|
done < <(json::config_load "$file" 2>/dev/null)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# modules/display.module.sh
|
|
||||||
# Display configuration — controls layout style per view
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# State — loaded once on first access
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
_DISPLAY_LOADED=false
|
|
||||||
declare -gA _DISPLAY_STYLES=()
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Load display config
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
function display::_load() {
|
|
||||||
$_DISPLAY_LOADED && return 0
|
|
||||||
_DISPLAY_LOADED=true
|
|
||||||
|
|
||||||
local display_file
|
|
||||||
display_file="$(ctx::display)"
|
|
||||||
[[ ! -f "$display_file" ]] && return 0
|
|
||||||
|
|
||||||
# Load styles per view via json_helper
|
|
||||||
local view style
|
|
||||||
while IFS='=' read -r view style; do
|
|
||||||
[[ -n "$view" && -n "$style" ]] && _DISPLAY_STYLES["$view"]="$style"
|
|
||||||
done < <(python3 "$(ctx::json_helper)" display_load "$display_file" 2>/dev/null)
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Accessors
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
# display::style <view>
|
|
||||||
# Returns: compact | table | minimal (default: compact)
|
|
||||||
function display::style() {
|
|
||||||
local view="${1:-}"
|
|
||||||
display::_load
|
|
||||||
echo "${_DISPLAY_STYLES[$view]:-compact}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# display::is_compact <view>
|
|
||||||
function display::is_compact() {
|
|
||||||
[[ "$(display::style "$1")" == "compact" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# display::is_table <view>
|
|
||||||
function display::is_table() {
|
|
||||||
[[ "$(display::style "$1")" == "table" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# display::render <view> <data> <compact_fn> <table_fn> [extra_args...]
|
|
||||||
# Generic dispatcher — calls compact or table render function
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
function display::render() {
|
|
||||||
local view="${1:-}" data="${2:-}" compact_fn="${3:-}" table_fn="${4:-}"
|
|
||||||
shift 4 || true
|
|
||||||
|
|
||||||
case "$(display::style "$view")" in
|
|
||||||
table)
|
|
||||||
declare -f "$table_fn" >/dev/null 2>&1 && \
|
|
||||||
"$table_fn" "$data" "$@" || \
|
|
||||||
"$compact_fn" "$data" "$@" # fallback to compact if no table fn
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
"$compact_fn" "$data" "$@"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ui/activity.module.sh — rendering for wgctl activity
|
# ui/activity.module.sh — rendering for wgctl activity
|
||||||
|
|
||||||
|
# ui::activity::peer_row <name_pad> <rx_pad> <tx_pad> <drops> <drop_word> <w_drops>
|
||||||
function ui::activity::peer_row() {
|
function ui::activity::peer_row() {
|
||||||
local name_pad="${1:-}" rx_pad="${2:-}" tx_pad="${3:-}" \
|
local name_pad="${1:-}" rx_pad="${2:-}" tx_pad="${3:-}" \
|
||||||
drops="${4:-0}" drop_word="${5:-drops}" w_drops="${6:-1}"
|
drops="${4:-0}" drop_word="${5:-drops}" w_drops="${6:-1}"
|
||||||
|
|
@ -9,78 +10,25 @@ function ui::activity::peer_row() {
|
||||||
"$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word"
|
"$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── _strip_ansi <string> → visible string
|
# ui::activity::service_row <dest_display> <drop_count> <drop_word> <drops_col> <w_drops>
|
||||||
# Used for measuring visible length of strings that may contain ANSI codes
|
|
||||||
function ui::activity::_visible_len() {
|
|
||||||
local s="$1"
|
|
||||||
printf "%b" "$s" | sed 's/\x1b\[[0-9;]*m//g' | wc -m | tr -d ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
# ui::activity::service_row
|
|
||||||
# dest_display may contain ANSI (when --ports passes dim suffix)
|
|
||||||
function ui::activity::service_row() {
|
function ui::activity::service_row() {
|
||||||
local dest_display="${1:-}" drop_count="${2:-0}" drop_word="${3:-drops}" \
|
local dest_display="${1:-}" drop_count="${2:-0}" drop_word="${3:-drops}" \
|
||||||
drops_col="${4:-30}" w_count="${5:-1}"
|
drops_col="${4:-30}" w_drops="${5:-1}"
|
||||||
|
|
||||||
|
# Align drop count with peer drop column
|
||||||
|
# Service row visible prefix: " → " (6 visible) + ${#dest_display}
|
||||||
|
# But "→" is 3 bytes, 1 visible — arrow_prefix bytes = 8, visible = 6
|
||||||
local arrow_prefix=" → "
|
local arrow_prefix=" → "
|
||||||
local prefix_bytes=${#arrow_prefix}
|
local prefix_bytes=${#arrow_prefix} # 8 bytes due to → being 3 bytes
|
||||||
# Measure visible length of dest (strip ANSI for correct padding)
|
local prefix_len=$(( prefix_bytes + ${#dest_display} ))
|
||||||
local dest_visible_len
|
|
||||||
dest_visible_len=$(ui::activity::_visible_len "$dest_display")
|
|
||||||
local prefix_len=$(( prefix_bytes + dest_visible_len ))
|
|
||||||
local pad_n=$(( drops_col - prefix_len ))
|
local pad_n=$(( drops_col - prefix_len ))
|
||||||
[[ $pad_n -lt 1 ]] && pad_n=1
|
[[ $pad_n -lt 1 ]] && pad_n=1
|
||||||
|
|
||||||
printf " \033[0;31m→\033[0m \033[0;31m%b\033[0m%*s \033[0;31m%${w_count}s %s\033[0m\n" \
|
printf " \033[2m→\033[0m %s%*s %${w_drops}s %s\n" \
|
||||||
"$dest_display" "$pad_n" "" "$drop_count" "$drop_word"
|
"$dest_display" "$pad_n" "" "$drop_count" "$drop_word"
|
||||||
}
|
}
|
||||||
|
|
||||||
function ui::activity::accept_row() {
|
# Table versions (kept for future display config)
|
||||||
local name_pad="${1:-}" bytes_in="${2:-}" bytes_out="${3:-}" \
|
|
||||||
conns="${4:-0}" w_count="${5:-4}"
|
|
||||||
|
|
||||||
local conn_word="conns"
|
|
||||||
[[ "$conns" -eq 1 ]] && conn_word="conn"
|
|
||||||
|
|
||||||
local spaces
|
|
||||||
spaces=$(printf '%*s' "${#name_pad}" '')
|
|
||||||
|
|
||||||
printf " \033[0;32m%s ↓%-10s ↑%-10s %${w_count}s %s\033[0m\n" \
|
|
||||||
"$spaces" "$bytes_in" "$bytes_out" "$conns" "$conn_word"
|
|
||||||
}
|
|
||||||
|
|
||||||
function ui::activity::accept_dest_row() {
|
|
||||||
local dest="${1:-}" bytes_orig="${2:-0}" bytes_reply="${3:-0}" \
|
|
||||||
count="${4:-0}" drops_col="${5:-40}" w_count="${6:-4}"
|
|
||||||
|
|
||||||
local conn_word="conns"
|
|
||||||
[[ "$count" -eq 1 ]] && conn_word="conn"
|
|
||||||
|
|
||||||
local arrow_prefix=" → "
|
|
||||||
local prefix_bytes=${#arrow_prefix}
|
|
||||||
# Measure visible length of dest (strip ANSI for correct padding)
|
|
||||||
local dest_visible_len
|
|
||||||
dest_visible_len=$(ui::activity::_visible_len "$dest")
|
|
||||||
local prefix_len=$(( prefix_bytes + dest_visible_len ))
|
|
||||||
local pad_n=$(( drops_col - prefix_len ))
|
|
||||||
[[ $pad_n -lt 1 ]] && pad_n=1
|
|
||||||
|
|
||||||
# Build bytes display
|
|
||||||
local bytes_display=""
|
|
||||||
if [[ "$bytes_orig" -gt 0 || "$bytes_reply" -gt 0 ]]; then
|
|
||||||
bytes_display=" "
|
|
||||||
[[ "$bytes_reply" -gt 0 ]] && bytes_display+="↓$(fmt::bytes "$bytes_reply") "
|
|
||||||
[[ "$bytes_orig" -gt 0 ]] && bytes_display+="↑$(fmt::bytes "$bytes_orig")"
|
|
||||||
bytes_display="${bytes_display% }"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use %b for dest to interpret ANSI, keep rest as %s/%d
|
|
||||||
printf " \033[0;32m→\033[0m \033[0;32m%b\033[0m%*s \033[0;32m%${w_count}s %-5s\033[0m%s\n" \
|
|
||||||
"$dest" "$pad_n" "" "$count" "$conn_word" "$bytes_display"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Table versions ──────────────────────────────────────
|
|
||||||
|
|
||||||
function ui::activity::header_table() {
|
function ui::activity::header_table() {
|
||||||
printf "\n %-24s %-14s %-14s %s\n" "PEER" "↓ RX" "↑ TX" "DROPS"
|
printf "\n %-24s %-14s %-14s %s\n" "PEER" "↓ RX" "↑ TX" "DROPS"
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ function ui::peer::status_color() {
|
||||||
elif [[ "$status" == "online"* ]]; then
|
elif [[ "$status" == "online"* ]]; then
|
||||||
echo "\033[1;32m"
|
echo "\033[1;32m"
|
||||||
else
|
else
|
||||||
echo "\033[2m"
|
echo "\033[2;37m"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
function ui::policy::list_row() {
|
|
||||||
local name="${1:-}" default_rule="${2:-}" strict="${3:-}" auto="${4:-}"
|
|
||||||
|
|
||||||
local rule_val="-"
|
|
||||||
[[ -n "$default_rule" ]] && rule_val="$default_rule"
|
|
||||||
|
|
||||||
local rule_padded
|
|
||||||
rule_padded=$(printf "%-16s" "$rule_val")
|
|
||||||
|
|
||||||
local strict_display
|
|
||||||
[[ "$strict" == "true" ]] && strict_display="yes" || strict_display="no"
|
|
||||||
local strict_padded
|
|
||||||
strict_padded=$(printf "%-4s" "$strict_display")
|
|
||||||
|
|
||||||
local auto_display=""
|
|
||||||
[[ "$auto" == "false" ]] && auto_display=" \033[2mauto:\033[0m no"
|
|
||||||
|
|
||||||
printf " %-14s \033[2mrule:\033[0m %s \033[2mstrict:\033[0m %s%s\n" \
|
|
||||||
"$name" "$rule_padded" "$strict_padded" "$auto_display"
|
|
||||||
}
|
|
||||||
|
|
||||||
function ui::policy::detail_field() {
|
|
||||||
local key="${1:-}" value="${2:-}"
|
|
||||||
ui::row "$key" "$value"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Table view
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function ui::policy::list_header_table() {
|
|
||||||
printf "\n %-16s %-8s %-14s %-8s %s\n" \
|
|
||||||
"NAME" "TUNNEL" "DEFAULT RULE" "STRICT" "AUTO"
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..60})"
|
|
||||||
}
|
|
||||||
|
|
||||||
function ui::policy::list_row_table() {
|
|
||||||
local name="${1:-}" tunnel="${2:-}" default_rule="${3:-}" \
|
|
||||||
strict="${4:-}" auto="${5:-}"
|
|
||||||
printf " %-16s %-8s %-14s %-8s %s\n" \
|
|
||||||
"$name" "$tunnel" "${default_rule:--}" "$strict" "$auto"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -392,24 +392,6 @@ function ui::rule::list_extends_detailed() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Table view
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function ui::rule::list_header_table() {
|
|
||||||
printf "\n %-20s %-6s %-6s %-8s %-20s %s\n" \
|
|
||||||
"NAME" "ALLOW" "BLOCK" "PEERS" "EXTENDS" "GROUP"
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..75})"
|
|
||||||
}
|
|
||||||
|
|
||||||
function ui::rule::list_row_table() {
|
|
||||||
local name="${1:-}" n_allows="${2:-0}" n_blocks="${3:-0}" \
|
|
||||||
peer_count="${4:-0}" extends="${5:-}" group="${6:-}"
|
|
||||||
printf " %-20s %-6s %-6s %-8s %-20s %s\n" \
|
|
||||||
"$name" "+${n_allows}" "-${n_blocks}" "$peer_count" \
|
|
||||||
"${extends:--}" "${group:--}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
# ======================================================
|
||||||
# Show helpers
|
# Show helpers
|
||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,33 @@ function ui::subnet::header() {
|
||||||
ui::divider 70
|
ui::divider 70
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ui::policy::list_row() {
|
||||||
|
local name="${1:-}" default_rule="${2:-}" strict="${3:-}" auto="${4:-}"
|
||||||
|
|
||||||
|
local rule_val="-"
|
||||||
|
[[ -n "$default_rule" ]] && rule_val="$default_rule"
|
||||||
|
|
||||||
|
local rule_padded
|
||||||
|
rule_padded=$(printf "%-16s" "$rule_val")
|
||||||
|
|
||||||
|
local strict_display
|
||||||
|
[[ "$strict" == "true" ]] && strict_display="yes" || strict_display="no"
|
||||||
|
local strict_padded
|
||||||
|
strict_padded=$(printf "%-4s" "$strict_display")
|
||||||
|
|
||||||
|
local auto_display=""
|
||||||
|
[[ "$auto" == "false" ]] && auto_display=" \033[2mauto:\033[0m no"
|
||||||
|
|
||||||
|
printf " %-14s \033[2mrule:\033[0m %s \033[2mstrict:\033[0m %s%s\n" \
|
||||||
|
"$name" "$rule_padded" "$strict_padded" "$auto_display"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::policy::detail_field() {
|
||||||
|
local key="${1:-}" value="${2:-}"
|
||||||
|
ui::row "$key" "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function ui::subnet::row() {
|
function ui::subnet::row() {
|
||||||
local display_name="${1:-}" subnet="${2:-}" type_key="${3:-}" \
|
local display_name="${1:-}" subnet="${2:-}" type_key="${3:-}" \
|
||||||
tunnel_mode="${4:-}" desc="${5:-}" is_group="${6:-false}"
|
tunnel_mode="${4:-}" desc="${5:-}" is_group="${6:-false}"
|
||||||
|
|
@ -183,20 +210,4 @@ function ui::subnet::show_peers_annotated() {
|
||||||
for peer in "${no_identity[@]}"; do
|
for peer in "${no_identity[@]}"; do
|
||||||
printf " · %b\n" "$peer"
|
printf " · %b\n" "$peer"
|
||||||
done
|
done
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# Table view
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
function ui::subnet::list_header_table() {
|
|
||||||
printf "\n %-14s %-20s %-8s %s\n" \
|
|
||||||
"TYPE" "CIDR" "TUNNEL" "DESCRIPTION"
|
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
|
||||||
}
|
|
||||||
|
|
||||||
function ui::subnet::list_row_table() {
|
|
||||||
local type="${1:-}" cidr="${2:-}" tunnel="${3:-}" desc="${4:-}"
|
|
||||||
printf " %-14s %-20s %-8s %s\n" \
|
|
||||||
"$type" "$cidr" "$tunnel" "${desc:--}"
|
|
||||||
}
|
}
|
||||||
12
wgctl
12
wgctl
|
|
@ -5,17 +5,12 @@ source "$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)/core.sh"
|
||||||
|
|
||||||
LOG_LEVEL=DEBUG
|
LOG_LEVEL=DEBUG
|
||||||
|
|
||||||
WGCTL_VERSION="0.7.1"
|
|
||||||
|
|
||||||
function wgctl::version() { echo "$WGCTL_VERSION"; }
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Modules
|
# Modules
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
load_module ip
|
load_module ip
|
||||||
load_module ui
|
load_module ui
|
||||||
load_module display
|
|
||||||
load_module config
|
load_module config
|
||||||
load_module keys
|
load_module keys
|
||||||
load_module peers
|
load_module peers
|
||||||
|
|
@ -43,6 +38,7 @@ declare -A CMD_ALIASES=(
|
||||||
[del]=remove
|
[del]=remove
|
||||||
[delete]=remove
|
[delete]=remove
|
||||||
[mv]=rename
|
[mv]=rename
|
||||||
|
[ls]=list
|
||||||
[show]=list
|
[show]=list
|
||||||
[monitor]=watch
|
[monitor]=watch
|
||||||
[ban]=block
|
[ban]=block
|
||||||
|
|
@ -52,6 +48,7 @@ declare -A CMD_ALIASES=(
|
||||||
[down]=service
|
[down]=service
|
||||||
[reload]=service
|
[reload]=service
|
||||||
[stat]=service
|
[stat]=service
|
||||||
|
[log]=service
|
||||||
[start]=service
|
[start]=service
|
||||||
[stop]=service
|
[stop]=service
|
||||||
[restart]=service
|
[restart]=service
|
||||||
|
|
@ -76,11 +73,6 @@ function wgctl::dispatch() {
|
||||||
local cmd
|
local cmd
|
||||||
cmd="$(wgctl::resolve_alias "$raw_cmd")"
|
cmd="$(wgctl::resolve_alias "$raw_cmd")"
|
||||||
|
|
||||||
# Resolve config-defined aliases (from wgctl.json commands section)
|
|
||||||
if [[ -n "${_COMMAND_ALIASES[$cmd]:-}" ]]; then
|
|
||||||
cmd="${_COMMAND_ALIASES[$cmd]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$cmd" in
|
case "$cmd" in
|
||||||
help) wgctl::help; return ;;
|
help) wgctl::help; return ;;
|
||||||
shell) : ;;
|
shell) : ;;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue