feat: peer endpoint history tracking and resolution
- daemon: update_peer_history() tracks all endpoints per peer - daemon: endpoint_index.json for O(1) IP -> peer name lookup - daemon: poll_handshakes updates history on every cycle - json_helper: peer_history_lookup() uses index, falls back to scan - resolve::endpoint_parts: step 3 checks peer history index - json.sh: json::peer_history_lookup wrapper - resolve: mobile peer IPs now resolve to peer name via history
This commit is contained in:
parent
c3cf5bc572
commit
8b47e55b4a
4 changed files with 158 additions and 27 deletions
23
core/json.sh
23
core/json.sh
|
|
@ -107,19 +107,22 @@ function json::policy_set_field() { python3 "$JSON_HELPER" policy_set_fiel
|
|||
function json::subnet_policy() { python3 "$JSON_HELPER" subnet_policy "$@" </dev/null; }
|
||||
|
||||
# Activity Monitor
|
||||
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::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::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::clean_handshakes() { python3 "$JSON_HELPER" clean_handshakes "$@" </dev/null; }
|
||||
function json::batch_resolve() { python3 "$JSON_HELPER" batch_resolve "$(ctx::hosts)" "$(ctx::net)" "$@" </dev/null; }
|
||||
# Peer History
|
||||
function json::peer_history_lookup() { python3 "$JSON_HELPER" peer_history_lookup "$(ctx::data)/peer-history" "$1" </dev/null; }
|
||||
|
||||
function json::clean_handshakes() { python3 "$JSON_HELPER" clean_handshakes "$@" </dev/null; }
|
||||
function json::batch_resolve() { python3 "$JSON_HELPER" batch_resolve "$(ctx::hosts)" "$(ctx::net)" "$@" </dev/null; }
|
||||
|
||||
function json::peer_transfer() {
|
||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||
|
|
|
|||
|
|
@ -1525,6 +1525,44 @@ def hosts_lookup(file, ip):
|
|||
else:
|
||||
print(str(entry))
|
||||
|
||||
# ======================================================
|
||||
# Peer History
|
||||
# ======================================================
|
||||
|
||||
def peer_history_lookup(history_dir, ip):
|
||||
"""
|
||||
Look up peer name for an endpoint IP using the index file.
|
||||
Falls back to scanning peer files if index doesn't exist.
|
||||
Returns peer name or empty string.
|
||||
"""
|
||||
import glob, os
|
||||
index_file = os.path.join(history_dir, "endpoint_index.json")
|
||||
try:
|
||||
if os.path.exists(index_file):
|
||||
with open(index_file) as f:
|
||||
index = json.load(f)
|
||||
result = index.get(ip, '')
|
||||
if result:
|
||||
print(result)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: scan peer files (rebuilds index implicitly)
|
||||
try:
|
||||
for hist_file in glob.glob(os.path.join(history_dir, "*.json")):
|
||||
if os.path.basename(hist_file) == "endpoint_index.json":
|
||||
continue
|
||||
try:
|
||||
with open(hist_file) as f:
|
||||
data = json.load(f)
|
||||
if ip in data.get("endpoints", {}):
|
||||
print(data.get("peer", ""))
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ======================================================
|
||||
|
||||
|
|
@ -1894,6 +1932,7 @@ commands = {
|
|||
'hosts_lookup': lambda args: hosts_lookup(args[0], args[1]),
|
||||
'clean_handshakes': lambda args: clean_handshakes(args[0], args[1] if len(args) > 1 else '300'),
|
||||
'batch_resolve': lambda args: batch_resolve(args[0], args[1], *args[2:]),
|
||||
'peer_history_lookup': lambda args: peer_history_lookup(args[0], args[1]),
|
||||
}
|
||||
|
||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ WG_HANDSHAKE_CHECK_SEC = int(os.environ.get("WG_HANDSHAKE_CHECK_TIME_SEC", "300"
|
|||
WG_WG_INTERFACE = os.environ.get("WG_WG_INTERFACE", "wg0") # WireGuard interface, not capture interface
|
||||
HS_CACHE_FILE = Path("/etc/wireguard/.wgctl/daemon/hs_cache.json")
|
||||
ENDPOINT_CACHE_FILE = Path("/etc/wireguard/.wgctl/daemon/endpoint_cache.json")
|
||||
PEER_HISTORY_DIR = Path("/etc/wireguard/.wgctl/peer-history")
|
||||
ENDPOINT_INDEX_FILE = PEER_HISTORY_DIR / "endpoint_index.json"
|
||||
|
||||
# ============================================
|
||||
# Logging
|
||||
|
|
@ -167,13 +169,15 @@ def poll_handshakes():
|
|||
Poll wg show latest-handshakes periodically.
|
||||
Log a handshake event only when gap > WG_HANDSHAKE_CHECK_SEC (new session).
|
||||
"""
|
||||
global _hs_last_logged
|
||||
global _hs_last_logged, _endpoint_index
|
||||
|
||||
_hs_last_logged = load_hs_cache()
|
||||
_hs_last_logged = load_hs_cache()
|
||||
_endpoint_index = load_endpoint_index()
|
||||
|
||||
pubkey_to_name = build_pubkey_to_name()
|
||||
log.info(f"Handshake poller started — {len(pubkey_to_name)} peers, "
|
||||
f"session threshold {WG_HANDSHAKE_CHECK_SEC}s")
|
||||
log.info(f"Endpoint index loaded — {len(_endpoint_index)} known endpoints")
|
||||
|
||||
while True:
|
||||
try:
|
||||
|
|
@ -203,12 +207,8 @@ def poll_handshakes():
|
|||
# Always update last seen
|
||||
_hs_last_logged[pubkey] = ts
|
||||
|
||||
if gap < WG_HANDSHAKE_CHECK_SEC:
|
||||
continue # keepalive, skip
|
||||
|
||||
# Get endpoint
|
||||
endpoint = get_endpoint(pubkey) or ''
|
||||
|
||||
if not endpoint:
|
||||
try:
|
||||
cache = json.loads(ENDPOINT_CACHE_FILE.read_text())
|
||||
|
|
@ -216,8 +216,14 @@ def poll_handshakes():
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Always update peer history + index
|
||||
if endpoint:
|
||||
update_peer_history(client, endpoint, ts)
|
||||
|
||||
# New session, log it
|
||||
if gap < WG_HANDSHAKE_CHECK_SEC:
|
||||
continue # keepalive, skip
|
||||
|
||||
# New session — log it
|
||||
entry = {
|
||||
"timestamp": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
|
||||
"ip": "",
|
||||
|
|
@ -238,7 +244,75 @@ def poll_handshakes():
|
|||
except Exception as e:
|
||||
log.error(f"Handshake poll error: {e}")
|
||||
|
||||
time.sleep(WG_HANDSHAKE_CHECK_SEC // 2) # poll at half the threshold
|
||||
time.sleep(WG_HANDSHAKE_CHECK_SEC // 2)
|
||||
|
||||
|
||||
# ============================================
|
||||
# Peer History
|
||||
# ============================================
|
||||
|
||||
def load_endpoint_index() -> dict:
|
||||
"""Load endpoint -> peer name index."""
|
||||
try:
|
||||
if ENDPOINT_INDEX_FILE.exists():
|
||||
return json.loads(ENDPOINT_INDEX_FILE.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
def save_endpoint_index(index: dict):
|
||||
"""Save endpoint -> peer name index."""
|
||||
try:
|
||||
PEER_HISTORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ENDPOINT_INDEX_FILE.write_text(json.dumps(index, indent=2))
|
||||
except Exception as e:
|
||||
log.error(f"Failed to save endpoint index: {e}")
|
||||
|
||||
# In-memory index — loaded once, updated on each new endpoint
|
||||
_endpoint_index: dict = {}
|
||||
|
||||
def update_peer_history(client: str, endpoint: str, ts: int):
|
||||
"""
|
||||
Update peer endpoint history and endpoint index.
|
||||
Called on every poll cycle to keep last_seen current.
|
||||
"""
|
||||
global _endpoint_index
|
||||
if not endpoint:
|
||||
return
|
||||
try:
|
||||
PEER_HISTORY_DIR.mkdir(parents=True, exist_ok=True)
|
||||
history_file = PEER_HISTORY_DIR / f"{client}.json"
|
||||
|
||||
if history_file.exists():
|
||||
try:
|
||||
data = json.loads(history_file.read_text())
|
||||
except Exception:
|
||||
data = {"peer": client, "endpoints": {}}
|
||||
else:
|
||||
data = {"peer": client, "endpoints": {}}
|
||||
|
||||
ts_iso = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
|
||||
eps = data.setdefault("endpoints", {})
|
||||
is_new = endpoint not in eps
|
||||
|
||||
if is_new:
|
||||
eps[endpoint] = {
|
||||
"first_seen": ts_iso,
|
||||
"last_seen": ts_iso,
|
||||
"count": 1
|
||||
}
|
||||
log.debug(f"New endpoint for {client}: {endpoint}")
|
||||
# Update in-memory index and persist
|
||||
_endpoint_index[endpoint] = client
|
||||
save_endpoint_index(_endpoint_index)
|
||||
else:
|
||||
eps[endpoint]["last_seen"] = ts_iso
|
||||
eps[endpoint]["count"] += 1
|
||||
|
||||
history_file.write_text(json.dumps(data, indent=2))
|
||||
except Exception as e:
|
||||
log.error(f"Failed to update peer history for {client}: {e}")
|
||||
|
||||
|
||||
# ============================================
|
||||
# Packet Handler
|
||||
|
|
|
|||
|
|
@ -93,16 +93,31 @@ function resolve::endpoint_parts() {
|
|||
[[ -z "$ip" ]] && echo "|" && return 0
|
||||
[[ "${_WGCTL_RAW:-false}" == "true" ]] && echo "${ip}|" && return 0
|
||||
|
||||
# Don't use cache for endpoint_parts — always resolve fresh
|
||||
local resolved=""
|
||||
[[ -f "$(ctx::hosts)" ]] && resolved=$(hosts::resolve_ip "$ip" 2>/dev/null || true)
|
||||
|
||||
# 1. hosts.json exact IP match
|
||||
[[ -f "$(ctx::hosts)" ]] && \
|
||||
resolved=$(hosts::resolve_ip "$ip" 2>/dev/null || true)
|
||||
|
||||
# 2. services.json match
|
||||
if [[ -z "$resolved" ]]; then
|
||||
resolved=$(net::reverse_lookup "$ip" "" "" 2>/dev/null) || resolved=""
|
||||
[[ "$resolved" == "$ip" ]] && resolved=""
|
||||
fi
|
||||
[[ "$resolved" == "$ip" ]] && resolved=""
|
||||
|
||||
# 3. Peer history index — O(1) lookup
|
||||
if [[ -z "$resolved" ]]; then
|
||||
local history_dir
|
||||
history_dir="$(ctx::data)/peer-history"
|
||||
if [[ -d "$history_dir" ]]; then
|
||||
resolved=$(json::peer_history_lookup "$ip" 2>/dev/null || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${ip}|${resolved}"
|
||||
}
|
||||
|
||||
|
||||
# resolve::clear_cache
|
||||
# Clears the resolution cache — call between commands if needed.
|
||||
function resolve::clear_cache() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue