diff --git a/commands/hosts.command.sh b/commands/hosts.command.sh new file mode 100644 index 0000000..55e5e7b --- /dev/null +++ b/commands/hosts.command.sh @@ -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 < [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 Show host entry details + show --subnet Show subnet entry details + show --port Show port entry details + add --ip --name Add a host entry + add --subnet --name + Add a subnet entry + add --port --name + Add a port entry + rm --ip Remove a host entry + rm --subnet Remove a subnet entry + rm --port Remove a port entry + +Options for add: + --ip IP address to map + --subnet Subnet CIDR to map (e.g. 10.0.0.0/24) + --port Port number to map (e.g. 443) + --name Display name (e.g. vodafone-wan) + --desc Optional description + --tag Tag (repeatable) + --tags 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}" +} \ No newline at end of file diff --git a/commands/logs.command.sh b/commands/logs.command.sh index b5b24e9..49289a4 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -144,7 +144,7 @@ function cmd::logs::show_fw_events() { local data 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 @@ -154,7 +154,11 @@ function cmd::logs::show_fw_events() { [[ -z "$ts" ]] && continue (( ${#client} > w_client )) && w_client=${#client} 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})" else [[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})" diff --git a/commands/watch.command.sh b/commands/watch.command.sh index 69a28fd..ba2f45b 100644 --- a/commands/watch.command.sh +++ b/commands/watch.command.sh @@ -33,7 +33,7 @@ Options: --blocked Show only blocked peer attempts --allowed Show only handshakes --restricted Show only firewall drop events - --raw Show raw IPs without service annotation + --raw Show raw IPs without host/service resolution Examples: wgctl watch @@ -62,7 +62,7 @@ function cmd::watch::run() { --blocked) blocked_only=true; shift ;; --allowed) allowed_only=true; shift ;; --restricted) restricted_only=true; shift ;; - --raw) raw=true; shift ;; + --raw) _WGCTL_RAW=true; shift ;; --help) cmd::watch::help; return ;; *) log::error "Unknown flag: $1" @@ -72,16 +72,11 @@ function cmd::watch::run() { esac done - local net_file="" - $raw || net_file="$(ctx::net)" - log::section "wgctl — Live Monitor (Ctrl+C to stop)" printf "\n" - # Fixed display widths for watch (dynamic measurement not possible in stream) local w_client=20 w_dest=18 - # Handshake poller (background) if ! $blocked_only && ! $restricted_only; then ( while true; do @@ -93,11 +88,10 @@ function cmd::watch::run() { local poller_pid=$! fi - # Event tailer (background) cmd::watch::_tail_events \ "$filter_name" "$filter_type" "$filter_peers" \ "$blocked_only" "$restricted_only" "$allowed_only" \ - "$net_file" "$w_client" "$w_dest" & + "$w_client" "$w_dest" & local tailer_pid=$! trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \ @@ -112,10 +106,7 @@ function cmd::watch::run() { function cmd::watch::_poll_handshakes() { local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" - local w_client="${4:-20}" w_dest="${5:-30}" - - local peer_set=() - [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers" + local w_client="${4:-20}" w_dest="${5:-18}" while IFS= read -r line; do local public_key ts @@ -123,7 +114,6 @@ function cmd::watch::_poll_handshakes() { ts=$(echo "$line" | awk '{print $2}') [[ -z "$ts" || "$ts" == "0" ]] && continue - # Find client by public key local client_name="" for conf in "$(ctx::clients)"/*.conf; do [[ -f "$conf" ]] || continue @@ -139,21 +129,28 @@ function cmd::watch::_poll_handshakes() { [[ -z "$client_name" ]] && continue [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue - # Dedup — only emit if handshake is new local safe_key safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1) local prev_ts_file="/tmp/wgctl_hs_${safe_key}" local prev_ts="0" [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file") [[ "$ts" == "$prev_ts" ]] && continue + + local gap=$(( ts - ${prev_ts:-0} )) echo "$ts" > "$prev_ts_file" + (( gap < ${WG_HANDSHAKE_CHECK_TIME_SEC:-300} )) && continue local ts_fmt ts_fmt=$(fmt::datetime_short "$ts") local endpoint 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" done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) @@ -166,10 +163,7 @@ function cmd::watch::_poll_handshakes() { function cmd::watch::_tail_events() { local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" 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 peer_set=() - [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers" + local w_client="${7:-20}" w_dest="${8:-18}" # Build ip->name map declare -A ip_to_name=() @@ -181,18 +175,6 @@ function cmd::watch::_tail_events() { [[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname" 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_WG=() @@ -227,7 +209,6 @@ function cmd::watch::_tail_events() { local client="${ip_to_name[$src_ip]:-$src_ip}" [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue - # Dedup local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}" local now; now=$(date +%s) local window=30 @@ -240,9 +221,8 @@ function cmd::watch::_tail_events() { local ts_fmt ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") - local svc_name dest_display - svc_name=$(_resolve_dest "$dest_ip" "$dest_port" "$proto") - dest_display=$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name") + local dest_display + dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto") 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 $allowed_only && [[ "$event" != "handshake" ]] && continue - # Dedup local wg_key="${client}:${endpoint}:${event}" local now; now=$(date +%s) 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" local ts_fmt 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" fi done diff --git a/core/context.sh b/core/context.sh index e6f4d5c..81c8ee2 100644 --- a/core/context.sh +++ b/core/context.sh @@ -46,6 +46,7 @@ function ctx::daemon() { echo "$_CTX_DAEMON"; } function ctx::net() { echo "$_CTX_NET"; } function ctx::identities() { echo "${_CTX_IDENTITY}"; } 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::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; } function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; } diff --git a/core/json.sh b/core/json.sh index 705d0f6..80401cb 100644 --- a/core/json.sh +++ b/core/json.sh @@ -110,6 +110,14 @@ function json::subnet_policy() { python3 "$JSON_HELPER" subnet_policy function json::activity_aggregate() { python3 "$JSON_HELPER" activity_aggregate "$@" 7 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__': diff --git a/modules/hosts.module.sh b/modules/hosts.module.sh new file mode 100644 index 0000000..7b959f9 --- /dev/null +++ b/modules/hosts.module.sh @@ -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" +} \ No newline at end of file diff --git a/modules/net.module.sh b/modules/net.module.sh index 3eea93c..d36092b 100644 --- a/modules/net.module.sh +++ b/modules/net.module.sh @@ -54,21 +54,6 @@ function net::annotate() { [[ -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() { local sign="${1:-}" entry="${2:-}" indent="${3:-6}" local ann @@ -99,4 +84,27 @@ function net::print_dns_redirect_full() { ann=$(net::annotate "$ip") printf " \033[0;36m↺\033[0m Redirect all DNS → %s\033[0;37m%s\033[0m\n" \ "$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" } \ No newline at end of file diff --git a/modules/resolve.module.sh b/modules/resolve.module.sh new file mode 100644 index 0000000..01b1c50 --- /dev/null +++ b/modules/resolve.module.sh @@ -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 [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 [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=() +} \ No newline at end of file diff --git a/modules/ui/hosts.module.sh b/modules/ui/hosts.module.sh new file mode 100644 index 0000000..79a9a52 --- /dev/null +++ b/modules/ui/hosts.module.sh @@ -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 +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})" +} \ No newline at end of file diff --git a/wgctl b/wgctl index bb82c82..fe9d054 100755 --- a/wgctl +++ b/wgctl @@ -22,6 +22,8 @@ load_module net load_module group load_module subnet load_module identity +load_module hosts +load_module resolve # ============================================ # Alias Map