feat: hosts.json IP resolution system
- wgctl hosts command (list, show, add, rm) with tags support - modules/resolve.module.sh — chain: hosts.json → services.json → raw IP - modules/hosts.module.sh — hosts::resolve_ip, hosts::lookup_ip - resolve::ip and resolve::dest used in watch, logs, activity - _WGCTL_RAW=true via --raw flag bypasses all resolution - json_helper.py: hosts_list, hosts_show, hosts_add, hosts_remove, hosts_lookup
This commit is contained in:
parent
6323f758ae
commit
b813810ff3
11 changed files with 625 additions and 56 deletions
291
commands/hosts.command.sh
Normal file
291
commands/hosts.command.sh
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# hosts.command.sh — manage host/IP display name mappings
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Lifecycle
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::hosts::on_load() {
|
||||||
|
flag::register --ip
|
||||||
|
flag::register --subnet
|
||||||
|
flag::register --port
|
||||||
|
flag::register --name
|
||||||
|
flag::register --desc
|
||||||
|
flag::register --tag
|
||||||
|
flag::register --tags
|
||||||
|
flag::register --force
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Help
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::hosts::help() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: wgctl hosts <subcommand> [options]
|
||||||
|
|
||||||
|
Manage host display names for IP resolution in logs, watch, and activity.
|
||||||
|
Maps IPs, subnets, and ports to human-readable names.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
list List all host entries
|
||||||
|
show --ip <ip> Show host entry details
|
||||||
|
show --subnet <cidr> Show subnet entry details
|
||||||
|
show --port <port> Show port entry details
|
||||||
|
add --ip <ip> --name <name> Add a host entry
|
||||||
|
add --subnet <cidr> --name <name>
|
||||||
|
Add a subnet entry
|
||||||
|
add --port <port> --name <name>
|
||||||
|
Add a port entry
|
||||||
|
rm --ip <ip> Remove a host entry
|
||||||
|
rm --subnet <cidr> Remove a subnet entry
|
||||||
|
rm --port <port> Remove a port entry
|
||||||
|
|
||||||
|
Options for add:
|
||||||
|
--ip <ip> IP address to map
|
||||||
|
--subnet <cidr> Subnet CIDR to map (e.g. 10.0.0.0/24)
|
||||||
|
--port <port> Port number to map (e.g. 443)
|
||||||
|
--name <name> Display name (e.g. vodafone-wan)
|
||||||
|
--desc <description> Optional description
|
||||||
|
--tag <tag> Tag (repeatable)
|
||||||
|
--tags <tag1,tag2> Tags (comma-separated)
|
||||||
|
|
||||||
|
Options for rm:
|
||||||
|
--force Skip confirmation
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
wgctl hosts list
|
||||||
|
wgctl hosts add --ip 148.69.46.73 --name vodafone-wan --desc "Vodafone WAN"
|
||||||
|
wgctl hosts add --ip 94.63.0.129 --name nuno-home --tags home,isp
|
||||||
|
wgctl hosts add --subnet 10.0.0.0/24 --name lan --desc "Local LAN"
|
||||||
|
wgctl hosts add --port 443 --name https
|
||||||
|
wgctl hosts show --ip 148.69.46.73
|
||||||
|
wgctl hosts rm --ip 148.69.46.73
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Run
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::hosts::run() {
|
||||||
|
local subcmd="${1:-list}"
|
||||||
|
shift || true
|
||||||
|
case "$subcmd" in
|
||||||
|
list) cmd::hosts::list "$@" ;;
|
||||||
|
show) cmd::hosts::show "$@" ;;
|
||||||
|
add) cmd::hosts::add "$@" ;;
|
||||||
|
rm|remove|del) cmd::hosts::rm "$@" ;;
|
||||||
|
help) cmd::hosts::help ;;
|
||||||
|
*)
|
||||||
|
log::error "Unknown subcommand: '${subcmd}'"
|
||||||
|
cmd::hosts::help
|
||||||
|
return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# List
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::hosts::list() {
|
||||||
|
local filter_tag=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--tag) filter_tag="$2"; shift 2 ;;
|
||||||
|
--help) cmd::hosts::help; return ;;
|
||||||
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local hosts_file
|
||||||
|
hosts_file="$(ctx::hosts)"
|
||||||
|
|
||||||
|
if [[ ! -f "$hosts_file" ]]; then
|
||||||
|
log::wg_warning "No hosts configured. Use 'wgctl hosts add' to add one."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local data
|
||||||
|
data=$(json::hosts_list "$hosts_file" 2>/dev/null)
|
||||||
|
[[ -z "$data" ]] && log::wg_warning "No hosts configured." && return 0
|
||||||
|
|
||||||
|
# Apply tag filter to data first
|
||||||
|
local filtered_data=""
|
||||||
|
while IFS='|' read -r type key name desc tags; do
|
||||||
|
[[ -z "$type" ]] && continue
|
||||||
|
[[ -n "$filter_tag" && "$tags" != *"$filter_tag"* ]] && continue
|
||||||
|
filtered_data+="${type}|${key}|${name}|${desc}|${tags}"$'\n'
|
||||||
|
done <<< "$data"
|
||||||
|
|
||||||
|
[[ -z "$filtered_data" ]] && log::wg_warning "No hosts found." && return 0
|
||||||
|
|
||||||
|
# Measure column widths from filtered data
|
||||||
|
local w_key=15 w_name=16 w_desc=10
|
||||||
|
while IFS='|' read -r type key name desc tags; do
|
||||||
|
[[ -z "$type" ]] && continue
|
||||||
|
(( ${#key} > w_key )) && w_key=${#key}
|
||||||
|
(( ${#name} > w_name )) && w_name=${#name}
|
||||||
|
local desc_len=${#desc}
|
||||||
|
[[ -z "$desc" ]] && desc_len=1 # "—" = 1 visible char
|
||||||
|
(( desc_len > w_desc )) && w_desc=$desc_len
|
||||||
|
done <<< "$filtered_data"
|
||||||
|
(( w_key += 2 ))
|
||||||
|
(( w_name += 2 ))
|
||||||
|
(( w_desc += 2 ))
|
||||||
|
|
||||||
|
log::section "Host Mappings"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local last_type="" found=false
|
||||||
|
while IFS='|' read -r type key name desc tags; do
|
||||||
|
[[ -z "$type" ]] && continue
|
||||||
|
found=true
|
||||||
|
|
||||||
|
# Section header when type changes
|
||||||
|
if [[ "$type" != "$last_type" ]]; then
|
||||||
|
[[ -n "$last_type" ]] && echo ""
|
||||||
|
ui::hosts::section_header "$type"
|
||||||
|
last_type="$type"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui::hosts::list_row "$type" "$key" "$name" "$desc" "$tags" \
|
||||||
|
"$w_key" "$w_name" "$w_desc"
|
||||||
|
|
||||||
|
done <<< "$filtered_data"
|
||||||
|
|
||||||
|
$found || log::wg_warning "No hosts configured."
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Table version (kept for future display config)
|
||||||
|
function cmd::hosts::_list_table() {
|
||||||
|
local hosts_file="${1:-}"
|
||||||
|
printf "\n %-6s %-18s %-16s %-30s %s\n" \
|
||||||
|
"TYPE" "KEY" "NAME" "DESCRIPTION" "TAGS"
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..80})"
|
||||||
|
|
||||||
|
while IFS='|' read -r type key name desc tags; do
|
||||||
|
[[ -z "$type" ]] && continue
|
||||||
|
printf " %-6s %-18s %-16s %-30s %s\n" \
|
||||||
|
"$type" "$key" "$name" "${desc:-—}" "${tags:-—}"
|
||||||
|
done < <(json::hosts_list "$hosts_file" 2>/dev/null)
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Show
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::hosts::show() {
|
||||||
|
local ip="" subnet="" port=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--ip) ip="$2"; shift 2 ;;
|
||||||
|
--subnet) subnet="$2"; shift 2 ;;
|
||||||
|
--port) port="$2"; shift 2 ;;
|
||||||
|
--help) cmd::hosts::help; return ;;
|
||||||
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local key entry_type
|
||||||
|
if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi
|
||||||
|
if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi
|
||||||
|
if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi
|
||||||
|
|
||||||
|
[[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1
|
||||||
|
|
||||||
|
hosts::require_exists "$entry_type" "$key" || return 1
|
||||||
|
|
||||||
|
log::section "${entry_type^}: ${key}"
|
||||||
|
printf "\n"
|
||||||
|
|
||||||
|
while IFS='|' read -r field val; do
|
||||||
|
case "$field" in
|
||||||
|
name) ui::row "Name" "${val:-—}" ;;
|
||||||
|
desc) ui::row "Description" "${val:-—}" ;;
|
||||||
|
tags) ui::row "Tags" "${val:-—}" ;;
|
||||||
|
esac
|
||||||
|
done < <(json::hosts_show "$(ctx::hosts)" "$key" "$entry_type")
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Add
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::hosts::add() {
|
||||||
|
local ip="" subnet="" port="" name="" desc="" tags=()
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--ip) ip="$2"; shift 2 ;;
|
||||||
|
--subnet) subnet="$2"; shift 2 ;;
|
||||||
|
--port) port="$2"; shift 2 ;;
|
||||||
|
--name) name="$2"; shift 2 ;;
|
||||||
|
--desc) desc="$2"; shift 2 ;;
|
||||||
|
--tag) tags+=("$2"); shift 2 ;;
|
||||||
|
--tags) IFS=',' read -ra t <<< "$2"; tags+=("${t[@]}"); shift 2 ;;
|
||||||
|
--help) cmd::hosts::help; return ;;
|
||||||
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||||
|
|
||||||
|
local key entry_type
|
||||||
|
if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi
|
||||||
|
if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi
|
||||||
|
if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi
|
||||||
|
|
||||||
|
[[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1
|
||||||
|
|
||||||
|
local tags_str
|
||||||
|
tags_str=$(IFS=','; echo "${tags[*]}")
|
||||||
|
|
||||||
|
json::hosts_add "$(ctx::hosts)" "$entry_type" "$key" "$name" "$desc" "$tags_str"
|
||||||
|
log::wg_success "Added ${entry_type}: ${key} → ${name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Remove
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::hosts::rm() {
|
||||||
|
local ip="" subnet="" port="" force=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--ip) ip="$2"; shift 2 ;;
|
||||||
|
--subnet) subnet="$2"; shift 2 ;;
|
||||||
|
--port) port="$2"; shift 2 ;;
|
||||||
|
--force) force=true; shift ;;
|
||||||
|
--help) cmd::hosts::help; return ;;
|
||||||
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local key entry_type
|
||||||
|
if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi
|
||||||
|
if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi
|
||||||
|
if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi
|
||||||
|
|
||||||
|
[[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1
|
||||||
|
|
||||||
|
hosts::require_exists "$entry_type" "$key" || return 1
|
||||||
|
|
||||||
|
if ! $force; then
|
||||||
|
read -r -p "Remove ${entry_type} '${key}'? [y/N] " confirm
|
||||||
|
case "$confirm" in
|
||||||
|
[yY]*) ;;
|
||||||
|
*) log::info "Aborted"; return 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
json::hosts_remove "$(ctx::hosts)" "$entry_type" "$key"
|
||||||
|
log::wg_success "Removed ${entry_type}: ${key}"
|
||||||
|
}
|
||||||
|
|
@ -144,7 +144,7 @@ function cmd::logs::show_fw_events() {
|
||||||
|
|
||||||
local data
|
local data
|
||||||
data=$(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
data=$(json::fw_events "$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
||||||
"$(ctx::clients)" "${net_file:-}" "$limit" 2>/dev/null)
|
"$(ctx::clients)" "${net_file:-}" "$(ctx::hosts)" "$limit" 2>/dev/null)
|
||||||
|
|
||||||
[[ -z "$data" ]] && return 0
|
[[ -z "$data" ]] && return 0
|
||||||
|
|
||||||
|
|
@ -154,7 +154,11 @@ function cmd::logs::show_fw_events() {
|
||||||
[[ -z "$ts" ]] && continue
|
[[ -z "$ts" ]] && continue
|
||||||
(( ${#client} > w_client )) && w_client=${#client}
|
(( ${#client} > w_client )) && w_client=${#client}
|
||||||
local dest_display
|
local dest_display
|
||||||
if [[ -n "$svc" ]]; then
|
local host_name
|
||||||
|
host_name=$(hosts::resolve_ip "$dest_ip")
|
||||||
|
if [[ -n "$host_name" ]]; then
|
||||||
|
dest_display="$host_name"
|
||||||
|
elif [[ -n "$svc" ]]; then
|
||||||
[[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})"
|
[[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})"
|
||||||
else
|
else
|
||||||
[[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})"
|
[[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ Options:
|
||||||
--blocked Show only blocked peer attempts
|
--blocked Show only blocked peer attempts
|
||||||
--allowed Show only handshakes
|
--allowed Show only handshakes
|
||||||
--restricted Show only firewall drop events
|
--restricted Show only firewall drop events
|
||||||
--raw Show raw IPs without service annotation
|
--raw Show raw IPs without host/service resolution
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl watch
|
wgctl watch
|
||||||
|
|
@ -62,7 +62,7 @@ function cmd::watch::run() {
|
||||||
--blocked) blocked_only=true; shift ;;
|
--blocked) blocked_only=true; shift ;;
|
||||||
--allowed) allowed_only=true; shift ;;
|
--allowed) allowed_only=true; shift ;;
|
||||||
--restricted) restricted_only=true; shift ;;
|
--restricted) restricted_only=true; shift ;;
|
||||||
--raw) raw=true; shift ;;
|
--raw) _WGCTL_RAW=true; shift ;;
|
||||||
--help) cmd::watch::help; return ;;
|
--help) cmd::watch::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
|
|
@ -72,16 +72,11 @@ function cmd::watch::run() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
local net_file=""
|
|
||||||
$raw || net_file="$(ctx::net)"
|
|
||||||
|
|
||||||
log::section "wgctl — Live Monitor (Ctrl+C to stop)"
|
log::section "wgctl — Live Monitor (Ctrl+C to stop)"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
|
||||||
# Fixed display widths for watch (dynamic measurement not possible in stream)
|
|
||||||
local w_client=20 w_dest=18
|
local w_client=20 w_dest=18
|
||||||
|
|
||||||
# Handshake poller (background)
|
|
||||||
if ! $blocked_only && ! $restricted_only; then
|
if ! $blocked_only && ! $restricted_only; then
|
||||||
(
|
(
|
||||||
while true; do
|
while true; do
|
||||||
|
|
@ -93,11 +88,10 @@ function cmd::watch::run() {
|
||||||
local poller_pid=$!
|
local poller_pid=$!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Event tailer (background)
|
|
||||||
cmd::watch::_tail_events \
|
cmd::watch::_tail_events \
|
||||||
"$filter_name" "$filter_type" "$filter_peers" \
|
"$filter_name" "$filter_type" "$filter_peers" \
|
||||||
"$blocked_only" "$restricted_only" "$allowed_only" \
|
"$blocked_only" "$restricted_only" "$allowed_only" \
|
||||||
"$net_file" "$w_client" "$w_dest" &
|
"$w_client" "$w_dest" &
|
||||||
local tailer_pid=$!
|
local tailer_pid=$!
|
||||||
|
|
||||||
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
|
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
|
||||||
|
|
@ -112,10 +106,7 @@ function cmd::watch::run() {
|
||||||
|
|
||||||
function cmd::watch::_poll_handshakes() {
|
function cmd::watch::_poll_handshakes() {
|
||||||
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
|
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
|
||||||
local w_client="${4:-20}" w_dest="${5:-30}"
|
local w_client="${4:-20}" w_dest="${5:-18}"
|
||||||
|
|
||||||
local peer_set=()
|
|
||||||
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
|
|
||||||
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
local public_key ts
|
local public_key ts
|
||||||
|
|
@ -123,7 +114,6 @@ function cmd::watch::_poll_handshakes() {
|
||||||
ts=$(echo "$line" | awk '{print $2}')
|
ts=$(echo "$line" | awk '{print $2}')
|
||||||
[[ -z "$ts" || "$ts" == "0" ]] && continue
|
[[ -z "$ts" || "$ts" == "0" ]] && continue
|
||||||
|
|
||||||
# Find client by public key
|
|
||||||
local client_name=""
|
local client_name=""
|
||||||
for conf in "$(ctx::clients)"/*.conf; do
|
for conf in "$(ctx::clients)"/*.conf; do
|
||||||
[[ -f "$conf" ]] || continue
|
[[ -f "$conf" ]] || continue
|
||||||
|
|
@ -139,21 +129,28 @@ function cmd::watch::_poll_handshakes() {
|
||||||
[[ -z "$client_name" ]] && continue
|
[[ -z "$client_name" ]] && continue
|
||||||
[[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue
|
[[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue
|
||||||
|
|
||||||
# Dedup — only emit if handshake is new
|
|
||||||
local safe_key
|
local safe_key
|
||||||
safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1)
|
safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1)
|
||||||
local prev_ts_file="/tmp/wgctl_hs_${safe_key}"
|
local prev_ts_file="/tmp/wgctl_hs_${safe_key}"
|
||||||
local prev_ts="0"
|
local prev_ts="0"
|
||||||
[[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file")
|
[[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file")
|
||||||
[[ "$ts" == "$prev_ts" ]] && continue
|
[[ "$ts" == "$prev_ts" ]] && continue
|
||||||
|
|
||||||
|
local gap=$(( ts - ${prev_ts:-0} ))
|
||||||
echo "$ts" > "$prev_ts_file"
|
echo "$ts" > "$prev_ts_file"
|
||||||
|
(( gap < ${WG_HANDSHAKE_CHECK_TIME_SEC:-300} )) && continue
|
||||||
|
|
||||||
local ts_fmt
|
local ts_fmt
|
||||||
ts_fmt=$(fmt::datetime_short "$ts")
|
ts_fmt=$(fmt::datetime_short "$ts")
|
||||||
local endpoint
|
local endpoint
|
||||||
endpoint=$(monitor::endpoint_for_key "$public_key")
|
endpoint=$(monitor::endpoint_for_key "$public_key")
|
||||||
|
|
||||||
ui::watch::wg_row "$ts_fmt" "$client_name" "${endpoint:-—}" "handshake" \
|
# Resolve endpoint
|
||||||
|
local endpoint_display
|
||||||
|
endpoint_display=$(resolve::ip "${endpoint:-}")
|
||||||
|
[[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:-—}"
|
||||||
|
|
||||||
|
ui::watch::wg_row "$ts_fmt" "$client_name" "$endpoint_display" "handshake" \
|
||||||
"$w_client" "$w_dest"
|
"$w_client" "$w_dest"
|
||||||
|
|
||||||
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
||||||
|
|
@ -166,10 +163,7 @@ function cmd::watch::_poll_handshakes() {
|
||||||
function cmd::watch::_tail_events() {
|
function cmd::watch::_tail_events() {
|
||||||
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
|
local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}"
|
||||||
local blocked_only="${4:-false}" restricted_only="${5:-false}" allowed_only="${6:-false}"
|
local blocked_only="${4:-false}" restricted_only="${5:-false}" allowed_only="${6:-false}"
|
||||||
local net_file="${7:-}" w_client="${8:-20}" w_dest="${9:-30}"
|
local w_client="${7:-20}" w_dest="${8:-18}"
|
||||||
|
|
||||||
local peer_set=()
|
|
||||||
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
|
|
||||||
|
|
||||||
# Build ip->name map
|
# Build ip->name map
|
||||||
declare -A ip_to_name=()
|
declare -A ip_to_name=()
|
||||||
|
|
@ -181,18 +175,6 @@ function cmd::watch::_tail_events() {
|
||||||
[[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname"
|
[[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname"
|
||||||
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
||||||
|
|
||||||
# Load net services if not --raw
|
|
||||||
declare -A _svc_cache=()
|
|
||||||
function _resolve_dest() {
|
|
||||||
local dest_ip="${1:-}" dest_port="${2:-}" proto="${3:-}"
|
|
||||||
[[ -z "$net_file" || ! -f "$net_file" ]] && echo "" && return
|
|
||||||
local key="${dest_ip}:${dest_port}:${proto}"
|
|
||||||
if [[ -z "${_svc_cache[$key]+x}" ]]; then
|
|
||||||
_svc_cache[$key]=$(net::reverse_lookup "$dest_ip" "$dest_port" "$proto" 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
echo "${_svc_cache[$key]:-}"
|
|
||||||
}
|
|
||||||
|
|
||||||
declare -A _WATCH_LAST_FW=()
|
declare -A _WATCH_LAST_FW=()
|
||||||
declare -A _WATCH_LAST_WG=()
|
declare -A _WATCH_LAST_WG=()
|
||||||
|
|
||||||
|
|
@ -227,7 +209,6 @@ function cmd::watch::_tail_events() {
|
||||||
local client="${ip_to_name[$src_ip]:-$src_ip}"
|
local client="${ip_to_name[$src_ip]:-$src_ip}"
|
||||||
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
|
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
|
||||||
|
|
||||||
# Dedup
|
|
||||||
local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}"
|
local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}"
|
||||||
local now; now=$(date +%s)
|
local now; now=$(date +%s)
|
||||||
local window=30
|
local window=30
|
||||||
|
|
@ -240,9 +221,8 @@ function cmd::watch::_tail_events() {
|
||||||
local ts_fmt
|
local ts_fmt
|
||||||
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
|
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
|
||||||
|
|
||||||
local svc_name dest_display
|
local dest_display
|
||||||
svc_name=$(_resolve_dest "$dest_ip" "$dest_port" "$proto")
|
dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto")
|
||||||
dest_display=$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name")
|
|
||||||
|
|
||||||
ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest"
|
ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest"
|
||||||
|
|
||||||
|
|
@ -259,17 +239,27 @@ function cmd::watch::_tail_events() {
|
||||||
$blocked_only && [[ "$event" != "attempt" ]] && continue
|
$blocked_only && [[ "$event" != "attempt" ]] && continue
|
||||||
$allowed_only && [[ "$event" != "handshake" ]] && continue
|
$allowed_only && [[ "$event" != "handshake" ]] && continue
|
||||||
|
|
||||||
# Dedup
|
|
||||||
local wg_key="${client}:${endpoint}:${event}"
|
local wg_key="${client}:${endpoint}:${event}"
|
||||||
local now; now=$(date +%s)
|
local now; now=$(date +%s)
|
||||||
local last="${_WATCH_LAST_WG[$wg_key]:-0}"
|
local last="${_WATCH_LAST_WG[$wg_key]:-0}"
|
||||||
(( now - last < 30 )) && continue
|
|
||||||
|
# Handshakes — only show if gap > 5min (new session)
|
||||||
|
# Attempts — shorter window (30s) since each attempt is meaningful
|
||||||
|
local window=30
|
||||||
|
[[ "$event" == "handshake" ]] && window="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}"
|
||||||
|
|
||||||
|
(( now - last < window )) && continue
|
||||||
_WATCH_LAST_WG["$wg_key"]="$now"
|
_WATCH_LAST_WG["$wg_key"]="$now"
|
||||||
|
|
||||||
local ts_fmt
|
local ts_fmt
|
||||||
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
|
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
|
||||||
|
|
||||||
ui::watch::wg_row "$ts_fmt" "$client" "${endpoint:-—}" "$event" \
|
# Resolve endpoint
|
||||||
|
local endpoint_display
|
||||||
|
endpoint_display=$(resolve::ip "${endpoint:-}")
|
||||||
|
[[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:-—}"
|
||||||
|
|
||||||
|
ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_display" "$event" \
|
||||||
"$w_client" "$w_dest"
|
"$w_client" "$w_dest"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ function ctx::daemon() { echo "$_CTX_DAEMON"; }
|
||||||
function ctx::net() { echo "$_CTX_NET"; }
|
function ctx::net() { echo "$_CTX_NET"; }
|
||||||
function ctx::identities() { echo "${_CTX_IDENTITY}"; }
|
function ctx::identities() { echo "${_CTX_IDENTITY}"; }
|
||||||
function ctx::subnets() { echo "${_CTX_DATA}/subnets.json"; }
|
function ctx::subnets() { echo "${_CTX_DATA}/subnets.json"; }
|
||||||
|
function ctx::hosts() { echo "${_CTX_DATA}/hosts.json"; }
|
||||||
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::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,14 @@ function json::subnet_policy() { python3 "$JSON_HELPER" subnet_policy
|
||||||
function json::activity_aggregate() { python3 "$JSON_HELPER" activity_aggregate "$@" </dev/null; }
|
function json::activity_aggregate() { python3 "$JSON_HELPER" activity_aggregate "$@" </dev/null; }
|
||||||
function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@" </dev/null; }
|
function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@" </dev/null; }
|
||||||
|
|
||||||
|
# Hosts Resolution
|
||||||
|
function json::hosts_list() { python3 "$JSON_HELPER" hosts_list "$@" </dev/null; }
|
||||||
|
function json::hosts_show() { python3 "$JSON_HELPER" hosts_show "$@" </dev/null; }
|
||||||
|
function json::hosts_add() { python3 "$JSON_HELPER" hosts_add "$@" </dev/null; }
|
||||||
|
function json::hosts_remove() { python3 "$JSON_HELPER" hosts_remove "$@" </dev/null; }
|
||||||
|
function json::hosts_exists() { python3 "$JSON_HELPER" hosts_exists "$@" </dev/null; }
|
||||||
|
function json::hosts_lookup() { python3 "$JSON_HELPER" hosts_lookup "$@" </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)" \
|
||||||
|
|
|
||||||
|
|
@ -2656,6 +2656,130 @@ def reverse_lookup(dest_ip, dest_port, proto):
|
||||||
for dest_display, count in sorted(svc_map.items(), key=lambda x: -x[1]):
|
for dest_display, count in sorted(svc_map.items(), key=lambda x: -x[1]):
|
||||||
print(f"service|{peer}|{dest_display}|{count}")
|
print(f"service|{peer}|{dest_display}|{count}")
|
||||||
|
|
||||||
|
def _hosts_read(file):
|
||||||
|
if not os.path.exists(file):
|
||||||
|
return {"hosts": {}, "subnets": {}, "ports": {}}
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Ensure all sections exist
|
||||||
|
data.setdefault("hosts", {})
|
||||||
|
data.setdefault("subnets", {})
|
||||||
|
data.setdefault("ports", {})
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
return {"hosts": {}, "subnets": {}, "ports": {}}
|
||||||
|
|
||||||
|
def _hosts_write(file, data):
|
||||||
|
with open(file, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
def hosts_list(file):
|
||||||
|
"""
|
||||||
|
List all host entries.
|
||||||
|
Output per line: type|key|name|desc|tags
|
||||||
|
"""
|
||||||
|
data = _hosts_read(file)
|
||||||
|
for ip, entry in sorted(data["hosts"].items()):
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
name = entry.get("name", "")
|
||||||
|
desc = entry.get("desc", "")
|
||||||
|
tags = ",".join(entry.get("tags", []))
|
||||||
|
else:
|
||||||
|
name = str(entry)
|
||||||
|
desc = ""
|
||||||
|
tags = ""
|
||||||
|
print(f"host|{ip}|{name}|{desc}|{tags}")
|
||||||
|
for subnet, entry in sorted(data["subnets"].items()):
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
name = entry.get("name", "")
|
||||||
|
desc = entry.get("desc", "")
|
||||||
|
tags = ",".join(entry.get("tags", []))
|
||||||
|
else:
|
||||||
|
name = str(entry)
|
||||||
|
desc = ""
|
||||||
|
tags = ""
|
||||||
|
print(f"subnet|{subnet}|{name}|{desc}|{tags}")
|
||||||
|
for port, entry in sorted(data["ports"].items()):
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
name = entry.get("name", "")
|
||||||
|
desc = entry.get("desc", "")
|
||||||
|
tags = ",".join(entry.get("tags", []))
|
||||||
|
else:
|
||||||
|
name = str(entry)
|
||||||
|
desc = ""
|
||||||
|
tags = ""
|
||||||
|
print(f"port|{port}|{name}|{desc}|{tags}")
|
||||||
|
|
||||||
|
def hosts_show(file, key, entry_type):
|
||||||
|
"""Show a single host entry. type: host|subnet|port"""
|
||||||
|
data = _hosts_read(file)
|
||||||
|
section_map = {"host": "hosts", "subnet": "subnets", "port": "ports"}
|
||||||
|
section = section_map.get(entry_type, "hosts")
|
||||||
|
entry = data[section].get(key)
|
||||||
|
if not entry:
|
||||||
|
print(f"Error: not found: {key}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
print(f"name|{entry.get('name', '')}")
|
||||||
|
print(f"desc|{entry.get('desc', '')}")
|
||||||
|
print(f"tags|{','.join(entry.get('tags', []))}")
|
||||||
|
else:
|
||||||
|
print(f"name|{entry}")
|
||||||
|
print(f"desc|")
|
||||||
|
print(f"tags|")
|
||||||
|
|
||||||
|
def hosts_add(file, entry_type, key, name, desc, tags):
|
||||||
|
"""
|
||||||
|
Add a host entry.
|
||||||
|
entry_type: host|subnet|port
|
||||||
|
key: IP, subnet CIDR, or port number
|
||||||
|
"""
|
||||||
|
data = _hosts_read(file)
|
||||||
|
section_map = {"host": "hosts", "subnet": "subnets", "port": "ports"}
|
||||||
|
section = section_map.get(entry_type, "hosts")
|
||||||
|
tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []
|
||||||
|
data[section][key] = {
|
||||||
|
"name": name,
|
||||||
|
"desc": desc,
|
||||||
|
"tags": tag_list
|
||||||
|
}
|
||||||
|
_hosts_write(file, data)
|
||||||
|
|
||||||
|
def hosts_remove(file, entry_type, key):
|
||||||
|
"""Remove a host entry."""
|
||||||
|
data = _hosts_read(file)
|
||||||
|
section_map = {"host": "hosts", "subnet": "subnets", "port": "ports"}
|
||||||
|
section = section_map.get(entry_type, "hosts")
|
||||||
|
if key not in data[section]:
|
||||||
|
print(f"Error: not found: {key}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
del data[section][key]
|
||||||
|
_hosts_write(file, data)
|
||||||
|
|
||||||
|
def hosts_exists(file, entry_type, key):
|
||||||
|
"""Check if a host entry exists."""
|
||||||
|
data = _hosts_read(file)
|
||||||
|
section_map = {"host": "hosts", "subnet": "subnets", "port": "ports"}
|
||||||
|
section = section_map.get(entry_type, "hosts")
|
||||||
|
print("true" if key in data[section] else "false")
|
||||||
|
|
||||||
|
def hosts_lookup(file, ip):
|
||||||
|
"""
|
||||||
|
Resolve an IP to a display name.
|
||||||
|
Checks hosts section only (exact match).
|
||||||
|
Returns name or empty string.
|
||||||
|
"""
|
||||||
|
data = _hosts_read(file)
|
||||||
|
entry = data["hosts"].get(ip)
|
||||||
|
if not entry:
|
||||||
|
print("")
|
||||||
|
return
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
print(entry.get("name", ip))
|
||||||
|
else:
|
||||||
|
print(str(entry))
|
||||||
|
|
||||||
commands = {
|
commands = {
|
||||||
'get': lambda args: get(args[0], args[1]),
|
'get': lambda args: get(args[0], args[1]),
|
||||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||||
|
|
@ -2800,6 +2924,14 @@ commands = {
|
||||||
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 ''
|
||||||
),
|
),
|
||||||
|
'hosts_list': lambda args: hosts_list(args[0]),
|
||||||
|
'hosts_show': lambda args: hosts_show(args[0], args[1], args[2]),
|
||||||
|
'hosts_add': lambda args: hosts_add(args[0], args[1], args[2], args[3],
|
||||||
|
args[4] if len(args) > 4 else '',
|
||||||
|
args[5] if len(args) > 5 else ''),
|
||||||
|
'hosts_remove': lambda args: hosts_remove(args[0], args[1], args[2]),
|
||||||
|
'hosts_exists': lambda args: hosts_exists(args[0], args[1], args[2]),
|
||||||
|
'hosts_lookup': lambda args: hosts_lookup(args[0], args[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
22
modules/hosts.module.sh
Normal file
22
modules/hosts.module.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# hosts.module.sh — host resolution helpers
|
||||||
|
|
||||||
|
function hosts::exists() {
|
||||||
|
local entry_type="${1:-host}" key="${2:-}"
|
||||||
|
[[ "$(json::hosts_exists "$(ctx::hosts)" "$entry_type" "$key")" == "true" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function hosts::require_exists() {
|
||||||
|
local entry_type="${1:-host}" key="${2:-}"
|
||||||
|
if ! hosts::exists "$entry_type" "$key"; then
|
||||||
|
log::error "${entry_type^} not found: ${key}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function hosts::resolve_ip() {
|
||||||
|
local ip="${1:-}"
|
||||||
|
[[ -z "$ip" ]] && return 0
|
||||||
|
[[ ! -f "$(ctx::hosts)" ]] && echo "" && return 0
|
||||||
|
json::hosts_lookup "$(ctx::hosts)" "$ip"
|
||||||
|
}
|
||||||
|
|
@ -54,21 +54,6 @@ function net::annotate() {
|
||||||
[[ -n "$ann" ]] && echo "${ann}" || echo ""
|
[[ -n "$ann" ]] && echo "${ann}" || echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# function net::print_entry() {
|
|
||||||
# local sign="${1:-}" entry="${2:-}" indent="${3:-6}"
|
|
||||||
|
|
||||||
# local ann
|
|
||||||
# ann=$(net::annotate "$entry")
|
|
||||||
|
|
||||||
# local color
|
|
||||||
# [[ "$sign" == "+" ]] && color="\033[0;32m" || color="\033[0;31m"
|
|
||||||
|
|
||||||
# local spaces
|
|
||||||
# spaces=$(printf '%*s' "$indent" '')
|
|
||||||
# printf "%s%b%s\033[0m %s\033[0;37m%s\033[0m\n" \
|
|
||||||
# "$spaces" "$color" "$sign" "$entry" "${ann:+ → ${ann}}"
|
|
||||||
# }
|
|
||||||
|
|
||||||
function net::print_entry() {
|
function net::print_entry() {
|
||||||
local sign="${1:-}" entry="${2:-}" indent="${3:-6}"
|
local sign="${1:-}" entry="${2:-}" indent="${3:-6}"
|
||||||
local ann
|
local ann
|
||||||
|
|
@ -99,4 +84,27 @@ function net::print_dns_redirect_full() {
|
||||||
ann=$(net::annotate "$ip")
|
ann=$(net::annotate "$ip")
|
||||||
printf " \033[0;36m↺\033[0m Redirect all DNS → %s\033[0;37m%s\033[0m\n" \
|
printf " \033[0;36m↺\033[0m Redirect all DNS → %s\033[0;37m%s\033[0m\n" \
|
||||||
"$ip" "${ann:+ → ${ann}}"
|
"$ip" "${ann:+ → ${ann}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function net::resolve_display() {
|
||||||
|
local ip="${1:-}" port="${2:-}" proto="${3:-}"
|
||||||
|
[[ -z "$ip" ]] && return 0
|
||||||
|
|
||||||
|
# --raw flag bypass
|
||||||
|
[[ "${_WGCTL_RAW:-false}" == "true" ]] && echo "$ip" && return 0
|
||||||
|
|
||||||
|
# 1. hosts.json exact IP match
|
||||||
|
local host_name=""
|
||||||
|
if [[ -f "$(ctx::hosts)" ]]; then
|
||||||
|
host_name=$(hosts::lookup_ip "$ip")
|
||||||
|
fi
|
||||||
|
[[ -n "$host_name" ]] && echo "$host_name" && return 0
|
||||||
|
|
||||||
|
# 2. services.json match
|
||||||
|
local svc_name=""
|
||||||
|
svc_name=$(net::reverse_lookup "$ip" "$port" "$proto" 2>/dev/null) || true
|
||||||
|
[[ -n "$svc_name" ]] && echo "$svc_name" && return 0
|
||||||
|
|
||||||
|
# 3. Raw IP fallback
|
||||||
|
echo "$ip"
|
||||||
}
|
}
|
||||||
67
modules/resolve.module.sh
Normal file
67
modules/resolve.module.sh
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# modules/resolve.module.sh — IP/host resolution chain
|
||||||
|
# Chains: hosts.json exact match → services.json match → raw IP
|
||||||
|
# Depends on: hosts.module.sh, net.module.sh
|
||||||
|
|
||||||
|
declare -gA _RESOLVE_CACHE=()
|
||||||
|
|
||||||
|
# resolve::ip <ip> [port] [proto]
|
||||||
|
# Resolves an IP to a display name using the full resolution chain.
|
||||||
|
# Returns raw IP if no match found.
|
||||||
|
# Respects _WGCTL_RAW=true to bypass resolution.
|
||||||
|
function resolve::ip() {
|
||||||
|
local ip="${1:-}" port="${2:-}" proto="${3:-}"
|
||||||
|
[[ -z "$ip" ]] && echo "" && return 0
|
||||||
|
[[ "${_WGCTL_RAW:-false}" == "true" ]] && echo "$ip" && return 0
|
||||||
|
|
||||||
|
local cache_key="${ip}:${port}:${proto}"
|
||||||
|
if [[ -z "${_RESOLVE_CACHE[$cache_key]+x}" ]]; then
|
||||||
|
local result=""
|
||||||
|
|
||||||
|
# 1. hosts.json exact IP match
|
||||||
|
if [[ -f "$(ctx::hosts)" ]]; then
|
||||||
|
result=$(hosts::resolve_ip "$ip")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. services.json match
|
||||||
|
if [[ -z "$result" ]]; then
|
||||||
|
result=$(net::reverse_lookup "$ip" "$port" "$proto" 2>/dev/null) || result=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Raw IP fallback
|
||||||
|
[[ -z "$result" ]] && result="$ip"
|
||||||
|
|
||||||
|
_RESOLVE_CACHE[$cache_key]="$result"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${_RESOLVE_CACHE[$cache_key]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# resolve::dest <ip> [port] [proto]
|
||||||
|
# Like resolve::ip but builds a formatted destination display string.
|
||||||
|
# e.g. "pihole:dns-udp" or "vodafone-wan" or "10.0.0.103:853/tcp"
|
||||||
|
function resolve::dest() {
|
||||||
|
local ip="${1:-}" port="${2:-}" proto="${3:-}"
|
||||||
|
[[ -z "$ip" ]] && echo "" && return 0
|
||||||
|
|
||||||
|
local name
|
||||||
|
name=$(resolve::ip "$ip" "$port" "$proto")
|
||||||
|
|
||||||
|
if [[ "$name" == "$ip" ]]; then
|
||||||
|
# No resolution — raw format
|
||||||
|
if [[ -n "$port" ]]; then
|
||||||
|
echo "${ip}:${port}/${proto}"
|
||||||
|
else
|
||||||
|
[[ -n "$proto" ]] && echo "${ip} (${proto})" || echo "$ip"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Resolved — just the name, no proto suffix
|
||||||
|
echo "$name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# resolve::clear_cache
|
||||||
|
# Clears the resolution cache — call between commands if needed.
|
||||||
|
function resolve::clear_cache() {
|
||||||
|
_RESOLVE_CACHE=()
|
||||||
|
}
|
||||||
44
modules/ui/hosts.module.sh
Normal file
44
modules/ui/hosts.module.sh
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ui/hosts.module.sh — rendering for hosts data
|
||||||
|
|
||||||
|
function ui::hosts::section_header() {
|
||||||
|
local type="${1:-}"
|
||||||
|
case "$type" in
|
||||||
|
host) printf " \033[0;37mHosts\033[0m\n" ;;
|
||||||
|
subnet) printf " \033[0;37mSubnets\033[0m\n" ;;
|
||||||
|
port) printf " \033[0;37mPorts\033[0m\n" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ui::hosts::list_row <type> <key> <name> <desc> <tags> <w_key> <w_name> <w_desc>
|
||||||
|
function ui::hosts::list_row() {
|
||||||
|
local type="${1:-}" key="${2:-}" name="${3:-}" desc="${4:-}" tags="${5:-}" \
|
||||||
|
w_key="${6:-15}" w_name="${7:-16}" w_desc="${8:-10}"
|
||||||
|
|
||||||
|
local key_pad name_pad desc_val
|
||||||
|
key_pad=$(printf "%-${w_key}s" "$key")
|
||||||
|
name_pad=$(printf "%-${w_name}s" "$name")
|
||||||
|
desc_val="${desc:--}"
|
||||||
|
|
||||||
|
local desc_pad_n=$(( w_desc - ${#desc_val} ))
|
||||||
|
[[ $desc_pad_n -lt 0 ]] && desc_pad_n=0
|
||||||
|
|
||||||
|
local tags_display=""
|
||||||
|
[[ -n "$tags" ]] && tags_display="\033[2m[${tags//,/, }]\033[0m"
|
||||||
|
|
||||||
|
printf " %s %s %s%*s %b\n" \
|
||||||
|
"$key_pad" "$name_pad" "$desc_val" "$desc_pad_n" "" "$tags_display"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Table version (kept for future display config)
|
||||||
|
function ui::hosts::list_row_table() {
|
||||||
|
local type="${1:-}" key="${2:-}" name="${3:-}" desc="${4:-}" tags="${5:-}"
|
||||||
|
printf " %-6s %-18s %-16s %-30s %s\n" \
|
||||||
|
"$type" "$key" "$name" "${desc:-—}" "${tags:-—}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ui::hosts::list_header_table() {
|
||||||
|
printf "\n %-6s %-18s %-16s %-30s %s\n" \
|
||||||
|
"TYPE" "KEY" "NAME" "DESCRIPTION" "TAGS"
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..80})"
|
||||||
|
}
|
||||||
2
wgctl
2
wgctl
|
|
@ -22,6 +22,8 @@ load_module net
|
||||||
load_module group
|
load_module group
|
||||||
load_module subnet
|
load_module subnet
|
||||||
load_module identity
|
load_module identity
|
||||||
|
load_module hosts
|
||||||
|
load_module resolve
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Alias Map
|
# Alias Map
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue