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: graham/towerops-web#169
This commit is contained in:
Graham McIntire 2026-03-25 14:30:13 -05:00 committed by graham
parent de455d1cf4
commit ce682991fe

View file

@ -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