feat(client): report per-protocol presence in HTB and network_monitor

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc9V1v7iXruubpoLiSE24M
This commit is contained in:
2026-09-21 09:23:33 -04:00
co-authored by Claude Sonnet 5
parent c61a571995
commit 1898bc52e5
4 changed files with 88 additions and 7 deletions
+23 -7
View File
@@ -25,6 +25,7 @@ from .. import __version__
# Import plugin system
from .plugin import PluginRegistry, PluginLoader, InfoPlugin, MonitorPlugin
from .connectivity import Connectivity, set_shared
# Constants
PORT = 50003
@@ -281,7 +282,16 @@ async def handle_update(conn: AsyncConnection, _msg: dict): # pyright: ignore[r
stop()
async def heartbeat_sender(conn: AsyncConnection, interval: int):
def build_heartbeat_msg(conn, interval: int, connectivity: Optional[Connectivity] = None) -> dict:
"""HTB payload; carries p4/p6 presence flags when the probe knows them."""
msg = {"acks": conn.ackcount, "rtt": conn.rtts[-1], "interval": interval}
if connectivity is not None:
msg.update(connectivity.flags())
return msg
async def heartbeat_sender(conn: AsyncConnection, interval: int,
connectivity: Optional[Connectivity] = None):
"""Send periodic heartbeats, retrying the connection if it is not open.
IPv6 connections that fail to open before their first successful send are
@@ -291,6 +301,7 @@ async def heartbeat_sender(conn: AsyncConnection, interval: int):
Args:
conn: Connection to send on
interval: Heartbeat interval in seconds
connectivity: Optional probe whose presence flags are attached to every heartbeat
"""
logger = logging.getLogger("hbc.heartbeat")
IPV6_EARLY_FAIL_LIMIT = 3
@@ -328,11 +339,7 @@ async def heartbeat_sender(conn: AsyncConnection, interval: int):
continue
try:
msg = {
"acks": conn.ackcount,
"rtt": conn.rtts[-1],
"interval": interval
}
msg = build_heartbeat_msg(conn, interval, connectivity)
await conn.sendto(msg, "HTB")
except asyncio.CancelledError:
@@ -620,8 +627,17 @@ async def async_main(args, config):
# Start async tasks
# Heartbeat senders (one per connection)
# Per-protocol internet presence probe (shared with the network_monitor plugin)
net_cfg = dict((config.get("plugins") or {}).get("network_monitor")
or config.get("network_monitor") or {})
connectivity = None
if net_cfg.get("probe_enabled", True):
connectivity = Connectivity.from_config(net_cfg)
set_shared(connectivity)
active_tasks.append(asyncio.create_task(connectivity.run(shutdown_event)))
for conn in connections:
task = asyncio.create_task(heartbeat_sender(conn, interval))
task = asyncio.create_task(heartbeat_sender(conn, interval, connectivity))
active_tasks.append(task)
# Plugin collector (uses all connections, but we'll use first one)
+11
View File
@@ -13,6 +13,7 @@ except ImportError:
psutil = None
from hbd.client.plugin import MonitorPlugin
from hbd.client.connectivity import get_shared
logger = logging.getLogger(__name__)
@@ -32,6 +33,10 @@ class NetworkMonitorPlugin(MonitorPlugin):
interfaces: List of interfaces to monitor (default: all)
include_connections: Include connection statistics (default: True)
include_addresses: Include interface addresses (default: False)
probe_enabled: Run the per-protocol internet presence probe (default: True)
probe_v4: IPv4 address to ping (default: 1.1.1.1)
probe_v6: IPv6 address to ping (default: 2606:4700:4700::1111)
probe_interval: Seconds between ICMP probes while healthy (default: 60)
"""
name = "network_monitor"
@@ -103,6 +108,7 @@ class NetworkMonitorPlugin(MonitorPlugin):
- etc.
- addresses: Interface address information (if include_addresses)
- Dict keyed by interface name with address details
- connectivity: per-protocol presence {ipv4, ipv6} -> {present, addr, gateway, rtt}
"""
if psutil is None:
logger.error("psutil not available")
@@ -228,6 +234,11 @@ class NetworkMonitorPlugin(MonitorPlugin):
except Exception as e:
logger.warning(f"Could not collect interface stats: {e}")
# Per-protocol internet presence, address and gateway (from the shared probe)
shared = get_shared()
if shared is not None:
metrics['connectivity'] = shared.as_metric()
return metrics