Compare commits

..

No commits in common. "a9dcba73f42610de7e0b8e8eb5d6d68e1b853fa0" and "6323f758ae6b19895efd79c2883c25269c0840eb" have entirely different histories.

11 changed files with 56 additions and 625 deletions

View file

@ -1,291 +0,0 @@
#!/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}"
}

View file

@ -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:-}" "$(ctx::hosts)" "$limit" 2>/dev/null)
"$(ctx::clients)" "${net_file:-}" "$limit" 2>/dev/null)
[[ -z "$data" ]] && return 0
@ -154,11 +154,7 @@ function cmd::logs::show_fw_events() {
[[ -z "$ts" ]] && continue
(( ${#client} > w_client )) && w_client=${#client}
local dest_display
local host_name
host_name=$(hosts::resolve_ip "$dest_ip")
if [[ -n "$host_name" ]]; then
dest_display="$host_name"
elif [[ -n "$svc" ]]; then
if [[ -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})"

View file

@ -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 host/service resolution
--raw Show raw IPs without service annotation
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) _WGCTL_RAW=true; shift ;;
--raw) raw=true; shift ;;
--help) cmd::watch::help; return ;;
*)
log::error "Unknown flag: $1"
@ -72,11 +72,16 @@ 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
@ -88,10 +93,11 @@ 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" \
"$w_client" "$w_dest" &
"$net_file" "$w_client" "$w_dest" &
local tailer_pid=$!
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
@ -106,7 +112,10 @@ 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:-18}"
local w_client="${4:-20}" w_dest="${5:-30}"
local peer_set=()
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
while IFS= read -r line; do
local public_key ts
@ -114,6 +123,7 @@ 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
@ -129,28 +139,21 @@ 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")
# 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" \
ui::watch::wg_row "$ts_fmt" "$client_name" "${endpoint:-}" "handshake" \
"$w_client" "$w_dest"
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
@ -163,7 +166,10 @@ 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 w_client="${7:-20}" w_dest="${8:-18}"
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"
# Build ip->name map
declare -A ip_to_name=()
@ -175,6 +181,18 @@ 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=()
@ -209,6 +227,7 @@ 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
@ -221,8 +240,9 @@ function cmd::watch::_tail_events() {
local ts_fmt
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
local dest_display
dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto")
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")
ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest"
@ -239,27 +259,17 @@ 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}"
# 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
(( now - last < 30 )) && 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)")
# 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" \
ui::watch::wg_row "$ts_fmt" "$client" "${endpoint:-}" "$event" \
"$w_client" "$w_dest"
fi
done

View file

@ -46,7 +46,6 @@ 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"; }

View file

@ -110,14 +110,6 @@ function json::subnet_policy() { python3 "$JSON_HELPER" subnet_policy
function json::activity_aggregate() { python3 "$JSON_HELPER" activity_aggregate "$@" </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() {
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
ACTIVITY_TOTAL_MED="$(config::activity_total_med)" \

View file

@ -2656,130 +2656,6 @@ def reverse_lookup(dest_ip, dest_port, proto):
for dest_display, count in sorted(svc_map.items(), key=lambda x: -x[1]):
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 = {
'get': lambda args: get(args[0], args[1]),
'set': lambda args: set_key(args[0], args[1], args[2]),
@ -2924,14 +2800,6 @@ commands = {
args[7] if len(args) > 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__':

View file

@ -1,22 +0,0 @@
#!/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"
}

View file

@ -54,6 +54,21 @@ 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
@ -84,27 +99,4 @@ 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"
}

View file

@ -1,67 +0,0 @@
#!/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=()
}

View file

@ -1,44 +0,0 @@
#!/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
View file

@ -22,8 +22,6 @@ load_module net
load_module group
load_module subnet
load_module identity
load_module hosts
load_module resolve
# ============================================
# Alias Map