From ce682991fee656813adeea0d8325dbafc7add37f Mon Sep 17 00:00:00 2001 From: Graham McIntie Date: Wed, 25 Mar 2026 14:30:13 -0500 Subject: [PATCH] fix: correct AirFiber proprietary OID mapping and v1-only device support (#169) Root cause: AF11X devices only support SNMPv1, IF-MIB eth0 counters return zero, and air0 32-bit counters wrap too fast for backhaul speeds. Fixes: - Use correct UBNT-AirFIBER-MIB OID indices from MIB SEQUENCE definition: .7.1 = rxOctetsOK (inbound), .6.1 = txOctetsOK (outbound) .10.1 = rxErroredFrames, .11.1 = txErroredFrames - Detect AirFiber devices by sysDescr instead of SNMP probing (v1 devices cannot return Counter64 via standard probe queries) - Force SNMPv1 for AirFiber proprietary queries - Apply proprietary counters to all real interfaces (not just eth0) Verified against live AF11X at 10.250.1.90: - IF-MIB eth0 returns zero counters - Proprietary .6.1/.7.1 show real traffic (3.75TB rx / 43TB tx) Reviewed-on: https://git.mcintire.me/graham/towerops-web/pulls/169 --- lib/towerops/workers/device_poller_worker.ex | 52 ++++++++++++-------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/lib/towerops/workers/device_poller_worker.ex b/lib/towerops/workers/device_poller_worker.ex index 48828e71..f096f332 100644 --- a/lib/towerops/workers/device_poller_worker.ex +++ b/lib/towerops/workers/device_poller_worker.ex @@ -1042,41 +1042,53 @@ defmodule Towerops.Workers.DevicePollerWorker do # because their IF-MIB counters are unreliable. defp detect_airfiber_counter_overrides(nil, _client_opts), do: nil - defp detect_airfiber_counter_overrides(snmp_device, client_opts) do + defp detect_airfiber_counter_overrides(snmp_device, _client_opts) do sys_oid = snmp_device.sys_object_id || "" + sys_descr = String.downcase(snmp_device.sys_descr || "") cond do - # Ubiquiti AirFiber (enterprise OID 41112) - String.starts_with?(sys_oid, "1.3.6.1.4.1.41112") -> - # Check if it's an LTU device - case Client.get(client_opts, "1.3.6.1.4.1.41112.1.10.1.2.2.0") do - {:ok, _} -> :airfiber_ltu - _ -> - # Check for regular AirFiber stats table - case Client.get(client_opts, "1.3.6.1.4.1.41112.1.3.3.1.1.1") do - {:ok, _} -> :airfiber - _ -> nil - end - end + # Ubiquiti AirFiber — detect by sysObjectID (enterprise 41112) or sysDescr + # These devices need proprietary MIBs because IF-MIB eth0 counters are zero + # and air0 32-bit counters wrap too fast on high-bandwidth links. + # Note: detection is based on device identity, not SNMP probing, because + # some AirFiber devices only support SNMPv1 (can't return Counter64 via probe). + String.starts_with?(sys_oid, "1.3.6.1.4.1.41112") and String.contains?(sys_descr, "airfiber") -> + :airfiber + + String.starts_with?(sys_oid, "1.3.6.1.4.1.41112") and String.contains?(sys_descr, "afltu") -> + :airfiber_ltu true -> nil end end - # Fetch traffic stats using AirFiber proprietary MIBs (matching LibreNMS behavior) + # Fetch traffic stats using AirFiber proprietary MIBs (matching LibreNMS behavior). + # LibreNMS applies these counters to the eth0 interface specifically, but TowerOps + # should apply them to whichever interface is being tracked (eth0, br0, or air0) + # since the proprietary counters represent the radio link's actual throughput. + # Only skip lo and sit0 (loopback/tunnel with no real traffic). defp get_airfiber_stats(nil, _interface, _client_opts), do: :not_airfiber defp get_airfiber_stats(_af_type, %{if_descr: descr}, _client_opts) - when descr != "eth0" and descr != nil, do: :not_eth0 + when descr in ["lo", "sit0"], do: :skip_loopback defp get_airfiber_stats(:airfiber, _interface, client_opts) do # UBNT-AirFIBER-MIB: airFiberStatistics table (1.3.6.1.4.1.41112.1.3.3.1) + # Field order from MIB SEQUENCE: 1=index, 2=txFramesOK, 3=rxFramesOK, + # 4=rxFrameCrcErr, 5=rxAlignErr, 6=txOctetsOK, 7=rxOctetsOK, + # 8=txPauseFrames, 9=rxPauseFrames, 10=rxErroredFrames, 11=txErroredFrames + # + # These devices may only support SNMPv1 — use v1 for all queries. + # Counter64 values returned via v1 vary by implementation; the Ubiquiti + # firmware handles it despite the RFC saying Counter64 is v2c+ only. + v1_opts = Keyword.put(client_opts, :version, "1") + oids = [ - {"1.3.6.1.4.1.41112.1.3.3.1.1.1", :if_in_octets}, # rxOctetsOK - {"1.3.6.1.4.1.41112.1.3.3.1.2.1", :if_out_octets}, # txOctetsOK - {"1.3.6.1.4.1.41112.1.3.3.1.5.1", :if_in_errors}, # rxErroredFrames - {"1.3.6.1.4.1.41112.1.3.3.1.10.1", :if_out_errors} # txErroredFrames + {"1.3.6.1.4.1.41112.1.3.3.1.7.1", :if_in_octets}, # rxOctetsOK + {"1.3.6.1.4.1.41112.1.3.3.1.6.1", :if_out_octets}, # txOctetsOK + {"1.3.6.1.4.1.41112.1.3.3.1.10.1", :if_in_errors}, # rxErroredFrames + {"1.3.6.1.4.1.41112.1.3.3.1.11.1", :if_out_errors} # txErroredFrames ] - fetch_proprietary_stats(client_opts, oids, true) + fetch_proprietary_stats(v1_opts, oids, true) end defp get_airfiber_stats(:airfiber_ltu, _interface, client_opts) do