From 94fd8780de3374b7f55d72172338689276be63a9 Mon Sep 17 00:00:00 2001 From: graham Date: Wed, 25 Mar 2026 12:25:13 -0500 Subject: [PATCH] fix/counter-wrap-detection (#164) Reviewed-on: https://git.mcintire.me/graham/towerops-web/pulls/164 --- lib/towerops/workers/device_poller_worker.ex | 12 +++++++++++- lib/towerops_web/channels/agent_channel.ex | 7 ++++++- lib/towerops_web/live/graph_live/show.ex | 12 +++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/towerops/workers/device_poller_worker.ex b/lib/towerops/workers/device_poller_worker.ex index facca833..14f4be9c 100644 --- a/lib/towerops/workers/device_poller_worker.ex +++ b/lib/towerops/workers/device_poller_worker.ex @@ -1326,7 +1326,10 @@ defmodule Towerops.Workers.DevicePollerWorker do end defp fetch_octet_counters(client_opts, hc_oids, std_oids) do - hc_results = Enum.map(hc_oids, &try_fetch_hc_counter(client_opts, &1)) + # Counter64 (HC) can't be encoded in SNMPv1 responses, so upgrade to v2c + # for HC queries. Most devices support v2c with the same community string. + hc_opts = upgrade_to_v2c_for_hc(client_opts) + hc_results = Enum.map(hc_oids, &try_fetch_hc_counter(hc_opts, &1)) hc_available = Enum.all?(hc_results, fn {_key, value} -> value != :not_available end) @@ -1338,6 +1341,13 @@ defmodule Towerops.Workers.DevicePollerWorker do end end + defp upgrade_to_v2c_for_hc(client_opts) do + case Keyword.get(client_opts, :version) do + v when v in ["1", :v1] -> Keyword.put(client_opts, :version, "2c") + _ -> client_opts + end + end + defp try_fetch_hc_counter(client_opts, {key, oid}) do case Client.get(client_opts, oid) do {:ok, value} -> diff --git a/lib/towerops_web/channels/agent_channel.ex b/lib/towerops_web/channels/agent_channel.ex index d9bf7ecf..f90a48fb 100644 --- a/lib/towerops_web/channels/agent_channel.ex +++ b/lib/towerops_web/channels/agent_channel.ex @@ -1083,9 +1083,14 @@ defmodule ToweropsWeb.AgentChannel do defp build_v2c_snmp_device(device, snmp_config) do community = snmp_config.community || "" + # Always use v2c for agent polling — Counter64 (HC counters) can't be + # encoded in SNMPv1 responses, causing 64-bit counter queries to fail. + # v2c uses the same community string and is supported by virtually all + # SNMP-capable devices. Without HC counters, 32-bit counters on fast + # links wrap undetectably (full cycle in ~69s at 500 Mbps). %SnmpDevice{ ip: device.ip_address, - version: device.snmp_version, + version: "2c", port: device.snmp_port || 161, community: community } diff --git a/lib/towerops_web/live/graph_live/show.ex b/lib/towerops_web/live/graph_live/show.ex index df6979f0..c0d4b3a7 100644 --- a/lib/towerops_web/live/graph_live/show.ex +++ b/lib/towerops_web/live/graph_live/show.ex @@ -1291,12 +1291,18 @@ defmodule ToweropsWeb.GraphLive.Show do # Poll octets for a single interface (try 64-bit, fall back to 32-bit) defp poll_single_interface_octets(interface, client_opts) do - # Try 64-bit counters first + # Counter64 (HC) can't be encoded in SNMPv1 — upgrade to v2c for HC queries + hc_opts = + case Keyword.get(client_opts, :version) do + v when v in ["1", :v1] -> Keyword.put(client_opts, :version, "2c") + _ -> client_opts + end + in_octets_oid = "1.3.6.1.2.1.31.1.1.1.6.#{interface.if_index}" out_octets_oid = "1.3.6.1.2.1.31.1.1.1.10.#{interface.if_index}" - with {:ok, in_octets} <- Snmp.Client.get(client_opts, in_octets_oid), - {:ok, out_octets} <- Snmp.Client.get(client_opts, out_octets_oid), + with {:ok, in_octets} <- Snmp.Client.get(hc_opts, in_octets_oid), + {:ok, out_octets} <- Snmp.Client.get(hc_opts, out_octets_oid), true <- is_number(in_octets) and is_number(out_octets) do build_interface_stat(interface, in_octets, out_octets) else