Packages

A comprehensive SNMP toolkit for Elixir featuring a unified API, pure Elixir implementation, and powerful device simulation. Perfect for network monitoring, testing, and development with support for SNMP operations, MIB management, and realistic device simulation.

Current section

Files

Jump to
snmpkit lib snmpkit snmp_sim device pdu_processor.ex
Raw

lib/snmpkit/snmp_sim/device/pdu_processor.ex

defmodule SnmpKit.SnmpSim.Device.PduProcessor do
@moduledoc """
Handles SNMP PDU processing for devices.
"""
require Logger
alias SnmpKit.SnmpSim.Device.WalkPduProcessor
alias SnmpKit.SnmpSim.MIB.SharedProfiles
import SnmpKit.SnmpSim.Device.OidHandler,
only: [
get_dynamic_oid_value: 2,
string_to_oid_list: 1
]
# SNMP Error Status constants
@no_error 0
@no_such_name 2
@read_only 4
@gen_err 5
def process_pdu(pdu, state) do
Logger.debug("process_pdu called with PDU type: #{inspect(pdu.type)}")
Logger.debug("Device has_walk_data: #{inspect(state.has_walk_data)}")
# Route to walk-based processor if device has walk data or manual oid_map
if state.has_walk_data or (Map.has_key?(state, :oid_map) and map_size(state.oid_map) > 0) do
Logger.debug("Processing PDU with walk-based processor for device #{state.device_type}")
case pdu.type do
:get_request -> WalkPduProcessor.process_get_request(pdu, state)
:get_next_request -> WalkPduProcessor.process_getnext_request(pdu, state)
:get_bulk_request -> WalkPduProcessor.process_getbulk_request(pdu, state)
:set_request -> WalkPduProcessor.process_set_request(pdu, state)
_ -> process_unsupported_pdu(pdu)
end
else
Logger.debug("Processing PDU with legacy processor for device #{state.device_type}")
varbinds_for_debug = Map.get(pdu, :varbinds, Map.get(pdu, :variable_bindings, []))
Logger.debug("PDU varbinds: #{inspect(varbinds_for_debug)}")
# Original complex processing for non-walk devices
case pdu.type do
:get_request ->
varbinds = Map.get(pdu, :varbinds, Map.get(pdu, :variable_bindings, []))
processed = process_get_request(varbinds, state)
response = create_get_response_with_fields(pdu, processed)
response
:get_next_request ->
varbinds = Map.get(pdu, :varbinds, Map.get(pdu, :variable_bindings, []))
processed = process_getnext_request(varbinds, state)
response = create_get_response_with_fields(pdu, processed)
response
:get_bulk_request ->
varbinds = Map.get(pdu, :varbinds, Map.get(pdu, :variable_bindings, []))
non_repeaters = Map.get(pdu, :non_repeaters, 0)
max_repetitions = Map.get(pdu, :max_repetitions, 0)
processed = process_getbulk_request(varbinds, state, non_repeaters, max_repetitions)
response = create_getbulk_response(pdu, processed)
response
:set_request ->
# For legacy devices, return readOnly error for all SET attempts
varbinds = Map.get(pdu, :varbinds, Map.get(pdu, :variable_bindings, []))
create_set_error_response(pdu, varbinds)
_ ->
process_unsupported_pdu(pdu)
end
end
end
def process_snmp_pdu(pdu, state) do
# Delegate to the new routing function
process_pdu(pdu, state)
end
defp process_get_request(variable_bindings, state) do
Logger.debug("PDU Processor: variable_bindings = #{inspect(variable_bindings)}")
normalized_bindings =
Enum.map(variable_bindings, fn
# Extract OID from 3-tuple
{oid, _type, _value} ->
Logger.debug("PDU Processor: Extracted OID from 3-tuple: #{inspect(oid)}")
oid
# Extract OID from 2-tuple (common in SNMP GET requests)
{oid, _type} ->
Logger.debug("PDU Processor: Extracted OID from 2-tuple: #{inspect(oid)}")
oid
# Use OID as-is if it's just an OID
oid ->
Logger.debug("PDU Processor: Using OID as-is: #{inspect(oid)}")
oid
end)
Enum.map(normalized_bindings, fn oid ->
Logger.debug("PDU Processor: Processing OID #{inspect(oid)}")
result = get_dynamic_oid_value(oid, state)
Logger.debug("PDU Processor: get_dynamic_oid_value returned #{inspect(result)}")
case result do
{:ok, {_oid_str, type, value}} ->
Logger.debug(
"DEBUG: get_dynamic_oid_value returned type=#{inspect(type)}, value=#{inspect(value)}"
)
{oid, type, value}
{:error, :no_such_name} ->
Logger.debug("DEBUG: OID #{inspect(oid)} not found, returning no_such_object")
{oid, :no_such_object, {:no_such_object, nil}}
other ->
Logger.debug(
"PDU Processor: Unexpected result for OID #{inspect(oid)}: #{inspect(other)}. Defaulting to null."
)
{oid, :null, :null}
end
end)
end
def process_getnext_request(variable_bindings, state) do
Enum.map(variable_bindings, fn varbind ->
oid = extract_varbind_oid(varbind)
oid_string =
case oid do
list when is_list(list) -> Enum.join(list, ".")
str when is_binary(str) -> str
_ -> raise "Invalid OID format"
end
try do
case SharedProfiles.get_next_oid(state.device_type, oid_string) do
{:ok, next_oid} ->
# Use get_dynamic_oid_value for legacy devices
case get_dynamic_oid_value(next_oid, state) do
{:ok, {_oid_str, type, value}} ->
next_oid_list = string_to_oid_list(next_oid)
{next_oid_list, type, value}
{:error, _} ->
# If we can't get the value, use fallback
SnmpKit.SnmpSim.Device.OidHandler.get_fallback_next_oid(oid, state)
end
:end_of_mib ->
{oid, :end_of_mib_view, {:end_of_mib_view, nil}}
_ ->
# For GETNEXT, if we can't find a next OID, it's end of MIB
{oid, :end_of_mib_view, {:end_of_mib_view, nil}}
end
catch
:error, reason ->
Logger.warning("Error processing GETNEXT for OID #{oid_string}: #{inspect(reason)}")
{oid, :no_such_object, {:no_such_object, nil}}
end
end)
end
defp extract_varbind_oid(varbind) do
case varbind do
{oid, _type, _value} -> oid
{oid, _value} -> oid
_ -> []
end
end
defp process_getbulk_request(varbinds, state, non_repeaters, max_repetitions) do
# Split varbinds into non-repeaters and repeaters
{non_repeater_varbinds, repeater_varbinds} = Enum.split(varbinds, non_repeaters)
# Process non-repeaters as GETNEXT
non_repeater_results = process_getnext_request(non_repeater_varbinds, state)
# Process repeaters - get multiple next OIDs for each
repeater_results =
if max_repetitions > 0 do
Enum.flat_map(repeater_varbinds, fn varbind ->
oid = extract_varbind_oid(varbind)
get_bulk_repetitions(oid, state, max_repetitions)
end)
else
[]
end
# Combine results
non_repeater_results ++ repeater_results
end
defp get_bulk_repetitions(start_oid, state, max_repetitions) do
# Get max_repetitions number of next OIDs starting from start_oid
{_final_oid, results} =
Enum.reduce_while(1..max_repetitions, {start_oid, []}, fn _, {current_oid, acc} ->
case get_next_oid_and_value(current_oid, state) do
{_next_oid, :end_of_mib_view, _} = result ->
# End of MIB, stop here
{:halt, {current_oid, Enum.reverse([result | acc])}}
{next_oid, type, _value} = result when type != :null ->
# Continue with next OID
{:cont, {next_oid, [result | acc]}}
_ ->
# Error or unexpected format
{:halt, {current_oid, Enum.reverse(acc)}}
end
end)
results
end
defp get_next_oid_and_value(oid, state) do
oid_string =
case oid do
list when is_list(list) -> Enum.join(list, ".")
str when is_binary(str) -> str
_ -> raise "Invalid OID format"
end
case get_dynamic_oid_value(oid_string, state) do
{:ok, _} ->
# For GETBULK, we need to get the NEXT OID after this one
case SharedProfiles.get_next_oid(state.device_type, oid_string) do
{:ok, next_oid_str} ->
next_oid_list = string_to_oid_list(next_oid_str)
case get_dynamic_oid_value(next_oid_str, state) do
{:ok, {_, next_type, next_value}} ->
{next_oid_list, next_type, next_value}
_ ->
{next_oid_list, :no_such_object, {:no_such_object, nil}}
end
:end_of_mib ->
{oid, :end_of_mib_view, {:end_of_mib_view, nil}}
_ ->
# For GETBULK, if we can't find a next OID, it's end of MIB
{oid, :end_of_mib_view, {:end_of_mib_view, nil}}
end
_ ->
# OID not found, try to get next
case SharedProfiles.get_next_oid(state.device_type, oid_string) do
{:ok, next_oid_str} ->
next_oid_list = string_to_oid_list(next_oid_str)
case get_dynamic_oid_value(next_oid_str, state) do
{:ok, {_, next_type, next_value}} ->
{next_oid_list, next_type, next_value}
_ ->
{next_oid_list, :no_such_object, {:no_such_object, nil}}
end
:end_of_mib ->
{oid, :end_of_mib_view, {:end_of_mib_view, nil}}
_ ->
# For GETBULK, if we can't find a next OID, it's end of MIB
{oid, :end_of_mib_view, {:end_of_mib_view, nil}}
end
end
end
defp create_getbulk_response(request_pdu, variable_bindings) do
# GETBULK responses always have error_status = 0 in SNMPv2c
%{
type: :get_response,
# Preserve version from request
version: Map.get(request_pdu, :version, 1),
request_id: request_pdu.request_id,
error_status: 0,
error_index: 0,
varbinds: Enum.reverse(variable_bindings)
}
end
defp create_get_response_with_fields(request_pdu, variable_bindings) do
# Initialize error status and index
{error_status, error_index, converted_bindings} =
Enum.reduce(Enum.with_index(variable_bindings), {@no_error, 0, []}, fn
{{oid, :end_of_mib_view, _}, index}, {_, _, acc} ->
oid_list =
case oid do
oid when is_list(oid) -> oid
oid when is_binary(oid) -> string_to_oid_list(oid)
_ -> oid
end
# Set error status and index for end_of_mib_view, use a special atom for encoding
{@no_error, index + 1, [{oid_list, :end_of_mib_view, {:end_of_mib_view, nil}} | acc]}
{{oid, :no_such_object, _}, index}, {_, _, acc} ->
oid_list =
case oid do
oid when is_list(oid) -> oid
oid when is_binary(oid) -> string_to_oid_list(oid)
_ -> oid
end
# Set error status and index for no_such_object, use a special atom for encoding
{@no_such_name, index + 1, [{oid_list, :no_such_object, {:no_such_object, nil}} | acc]}
{{oid, :no_such_instance, _}, index}, {_, _, acc} ->
oid_list =
case oid do
oid when is_list(oid) -> oid
oid when is_binary(oid) -> string_to_oid_list(oid)
_ -> oid
end
# Set error status and index for no_such_instance, use a special atom for encoding
{@no_such_name, index + 1,
[{oid_list, :no_such_instance, {:no_such_instance, nil}} | acc]}
{{oid, type, value}, _index}, {status, err_index, acc} ->
oid_list =
case oid do
oid when is_list(oid) -> oid
oid when is_binary(oid) -> string_to_oid_list(oid)
_ -> oid
end
# Keep track of error status if already set, otherwise no error
{status, err_index, [{oid_list, type, value} | acc]}
{varbind, _index}, {status, err_index, acc} ->
# Keep track of error status if already set
{status, err_index, [varbind | acc]}
end)
converted_bindings = Enum.reverse(converted_bindings)
# Create response format expected by tests (with :type and :varbinds fields)
response_pdu = %{
type: :get_response,
version: Map.get(request_pdu, :version, 1),
community: Map.get(request_pdu, :community, "public"),
request_id: Map.get(request_pdu, :request_id, 0),
varbinds: converted_bindings,
error_status: error_status,
error_index: error_index
}
response_pdu
end
defp create_set_error_response(request_pdu, variable_bindings) do
# SET responses always have error_status = 0 in SNMPv2c
%{
type: :get_response,
# Preserve version from request
version: Map.get(request_pdu, :version, 1),
request_id: request_pdu.request_id,
error_status: @read_only,
error_index: 0,
varbinds: Enum.reverse(variable_bindings)
}
end
defp process_unsupported_pdu(pdu) do
# Return error response for unsupported PDU types
%{
type: :get_response,
version: Map.get(pdu, :version, 1),
community: Map.get(pdu, :community, "public"),
request_id: Map.get(pdu, :request_id, 0),
varbinds: [],
error_status: @gen_err,
error_index: 0
}
end
end