feat: add SNMPv3 support to discovery and SNMP client

The discovery process was only passing SNMPv2c credentials (community
string) to the SNMP client, causing SNMPv3 devices to fail discovery
even when v3 credentials were configured.

Changes:
- Updated build_client_opts in discovery.ex to include SNMPv3 credentials
  (security_name, security_level, auth_protocol, auth_password,
   priv_protocol, priv_password) when version is "3"
- Updated build_snmp_opts in client.ex to pass v3 credentials to SnmpKit
- Added parse functions for security_level, auth_protocol, and priv_protocol
- Updated connection_opts typespec to include v3 parameters
- Fixed parameter names to match SnmpKit expectations (security_name vs sec_name)

This enables full SNMPv3 discovery on devices like MikroTik routers that
use v3 authentication, allowing them to discover the same sensors and
data that SNMPv2c would show.
This commit is contained in:
Graham McIntire 2026-02-04 18:18:37 -06:00
parent 8e0fad599e
commit ff89415ed3
No known key found for this signature in database
2 changed files with 66 additions and 7 deletions

View file

@ -10,11 +10,17 @@ defmodule Towerops.Snmp.Client do
@type connection_opts :: [
ip: String.t(),
community: String.t(),
version: String.t(),
port: non_neg_integer(),
transport: String.t(),
timeout: non_neg_integer()
timeout: non_neg_integer(),
community: String.t(),
security_name: String.t(),
security_level: String.t(),
auth_protocol: String.t(),
auth_password: String.t(),
priv_protocol: String.t(),
priv_password: String.t()
]
@type oid :: String.t() | [non_neg_integer()]
@ -417,25 +423,62 @@ defmodule Towerops.Snmp.Client do
defp build_snmp_opts(opts) do
version = parse_version(Keyword.fetch!(opts, :version))
community = Keyword.fetch!(opts, :community)
port = Keyword.get(opts, :port, 161)
timeout = Keyword.get(opts, :timeout, @default_timeout)
[
community: community,
base_opts = [
version: version,
port: port,
timeout: timeout,
retries: @default_retries
]
# Add version-specific authentication options
case version do
:v3 ->
base_opts ++
[
security_name: Keyword.fetch!(opts, :security_name),
security_level: parse_security_level(Keyword.fetch!(opts, :security_level)),
auth_protocol: parse_auth_proto(Keyword.get(opts, :auth_protocol)),
auth_password: Keyword.get(opts, :auth_password),
priv_protocol: parse_priv_proto(Keyword.get(opts, :priv_protocol)),
priv_password: Keyword.get(opts, :priv_password)
]
_ ->
base_opts ++ [community: Keyword.fetch!(opts, :community)]
end
end
defp parse_version("1"), do: :v1
defp parse_version("2c"), do: :v2c
defp parse_version("3"), do: :v3
defp parse_version(:v1), do: :v1
defp parse_version(:v2c), do: :v2c
defp parse_version(:v3), do: :v3
defp parse_version(_other), do: :v2c
defp parse_security_level("noAuthNoPriv"), do: :no_auth_no_priv
defp parse_security_level("authNoPriv"), do: :auth_no_priv
defp parse_security_level("authPriv"), do: :auth_priv
defp parse_security_level(_), do: :no_auth_no_priv
defp parse_auth_proto("MD5"), do: :md5
defp parse_auth_proto("SHA"), do: :sha
defp parse_auth_proto("SHA-224"), do: :sha224
defp parse_auth_proto("SHA-256"), do: :sha256
defp parse_auth_proto("SHA-384"), do: :sha384
defp parse_auth_proto("SHA-512"), do: :sha512
defp parse_auth_proto(_), do: :sha256
defp parse_priv_proto("DES"), do: :des
defp parse_priv_proto("AES"), do: :aes
defp parse_priv_proto("AES-192"), do: :aes192
defp parse_priv_proto("AES-256"), do: :aes256
defp parse_priv_proto("AES-256-C"), do: :aes256c
defp parse_priv_proto(_), do: :aes
@doc """
Resolve MIB name to numeric OID using SnmpKit.
Handles MODULE-NAME::objectName format by stripping module prefix.

View file

@ -382,12 +382,28 @@ defmodule Towerops.Snmp.Discovery do
# Get SNMP config with hierarchical fallback (device -> site -> organization)
snmp_config = Devices.get_snmp_config(device)
[
base_opts = [
ip: device.ip_address,
community: snmp_config.community,
version: snmp_config.version,
port: device.snmp_port || 161
]
# Add version-specific credentials
if snmp_config.version == "3" do
v3_config = Devices.get_snmpv3_config(device)
base_opts ++
[
security_name: v3_config.username,
security_level: v3_config.security_level,
auth_protocol: v3_config.auth_protocol,
auth_password: v3_config.auth_password,
priv_protocol: v3_config.priv_protocol,
priv_password: v3_config.priv_password
]
else
base_opts ++ [community: snmp_config.community]
end
end
@spec discover_system(Client.connection_opts()) :: {:ok, system_info()} | {:error, term()}