defmodule SnmpKit.SnmpLib.PDU do @moduledoc """ SNMP PDU (Protocol Data Unit) encoding and decoding with RFC compliance. Provides comprehensive SNMP PDU functionality combining the best features from multiple SNMP implementations. Supports SNMPv1 and SNMPv2c protocols with high-performance encoding/decoding, robust error handling, and full RFC compliance. ## API Documentation ### PDU Structure All PDU functions in this library use a **consistent map structure** with these fields: ```elixir %{ type: :get_request | :get_next_request | :get_response | :set_request | :get_bulk_request, request_id: non_neg_integer(), error_status: 0..18, error_index: non_neg_integer(), varbinds: [varbind()], # GETBULK only: non_repeaters: non_neg_integer(), # Optional, GETBULK requests only max_repetitions: non_neg_integer() # Optional, GETBULK requests only } ``` **IMPORTANT**: Always use the `:type` field (not `:pdu_type`) with atom values. ### Variable Bindings Format Variable bindings (`varbinds`) support two formats: - **2-tuple format**: `{oid, value}` - Used for responses and simple cases - **3-tuple format**: `{oid, type, value}` - Used for requests with explicit type info ```elixir # Request varbinds (3-tuple with type information) [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}] # Response varbinds (2-tuple format) [{[1, 3, 6, 1, 2, 1, 1, 1, 0], "Linux server"}] # Response varbinds (3-tuple format also supported) [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "Linux server"}] ``` ### Message Structure SNMP messages have this structure: ```elixir %{ version: 0 | 1, # 0 = SNMPv1, 1 = SNMPv2c community: binary(), # Community string pdu: pdu() # PDU map as described above } ``` ## Examples ### Building PDUs ```elixir # GET request for system description pdu = SnmpKit.SnmpLib.PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 123) # GETBULK request for interface table pdu = SnmpKit.SnmpLib.PDU.build_get_bulk_request([1, 3, 6, 1, 2, 1, 2, 2, 1], 124, 0, 10) # SET request pdu = SnmpKit.SnmpLib.PDU.build_set_request([1, 3, 6, 1, 2, 1, 1, 5, 0], {:octet_string, "New Name"}, 125) ``` ### Building Messages ```elixir # Complete SNMP message {:ok, message} = SnmpKit.SnmpLib.PDU.build_message(pdu, "public", :v2c) # Encode to binary {:ok, packet} = SnmpKit.SnmpLib.PDU.encode_message(message) # Decode from binary {:ok, decoded_message} = SnmpKit.SnmpLib.PDU.decode_message(packet) ``` ### Error Responses ```elixir # Create error response error_pdu = SnmpKit.SnmpLib.PDU.create_error_response(original_pdu, :no_such_name, 1) ``` """ alias SnmpKit.SnmpLib.PDU.Builder alias SnmpKit.SnmpLib.PDU.Constants alias SnmpKit.SnmpLib.PDU.Decoder alias SnmpKit.SnmpLib.PDU.Encoder # Re-export types from Constants @type message :: Constants.message() @type pdu :: Constants.pdu() @type varbind :: Constants.varbind() @type oid :: Constants.oid() @type snmp_value :: Constants.snmp_value() @type snmp_type :: Constants.snmp_type() @type error_status :: Constants.error_status() ## Public API - Encoding/Decoding @doc """ Encodes an SNMP message to binary format. ## Examples iex> message = %{version: 1, community: "public", pdu: %{type: :get_request, request_id: 123, error_status: 0, error_index: 0, varbinds: []}} iex> {:ok, _binary} = SnmpKit.SnmpLib.PDU.encode_message(message) """ @spec encode_message(message()) :: {:ok, binary()} | {:error, atom()} def encode_message(message), do: Encoder.encode_message(message) @doc """ Decodes an SNMP message from binary format. ## Examples iex> {:ok, binary} = SnmpKit.SnmpLib.PDU.encode_message(%{version: 1, community: "public", pdu: %{type: :get_request, request_id: 123, error_status: 0, error_index: 0, varbinds: []}}) iex> {:ok, _message} = SnmpKit.SnmpLib.PDU.decode_message(binary) """ @spec decode_message(binary()) :: {:ok, message()} | {:error, atom()} def decode_message(data), do: Decoder.decode_message(data) @doc """ Alias for encode_message/1. """ @spec encode(message()) :: {:ok, binary()} | {:error, atom()} def encode(message), do: Encoder.encode(message) @doc """ Alias for decode_message/1. """ @spec decode(binary()) :: {:ok, message()} | {:error, atom()} def decode(data), do: Decoder.decode(data) @doc """ Legacy alias for encode/1. """ @spec encode_snmp_packet(message()) :: {:ok, binary()} | {:error, atom()} def encode_snmp_packet(message), do: Encoder.encode_snmp_packet(message) @doc """ Legacy alias for decode/1. """ @spec decode_snmp_packet(binary()) :: {:ok, message()} | {:error, atom()} def decode_snmp_packet(data), do: Decoder.decode_snmp_packet(data) ## Public API - Building PDUs and Messages @doc """ Builds a GET request PDU. ## Parameters - `oid`: Single OID as list of integers - `request_id`: Unique request identifier ## Examples iex> pdu = SnmpKit.SnmpLib.PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 123) iex> pdu.type :get_request """ @spec build_get_request(oid(), non_neg_integer()) :: pdu() def build_get_request(oid, request_id), do: Builder.build_get_request(oid, request_id) @doc """ Builds a GET request PDU with multiple varbinds. ## Parameters - `varbinds`: List of variable bindings in format `{oid, type, value}` - `request_id`: Unique request identifier ## Examples iex> varbinds = [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, :null}] iex> pdu = SnmpKit.SnmpLib.PDU.build_get_request_multi(varbinds, 123) iex> pdu.type :get_request """ @spec build_get_request_multi([varbind()], non_neg_integer()) :: pdu() def build_get_request_multi(varbinds, request_id) do Builder.build_get_request_multi(varbinds, request_id) end @doc """ Builds a GETNEXT request PDU. ## Parameters - `oid_or_oids`: Single OID list or list of OID lists to request - `request_id`: Unique request identifier ## Examples iex> pdu = SnmpKit.SnmpLib.PDU.build_get_next_request([1, 3, 6, 1, 2, 1, 1], 123) iex> pdu.type :get_next_request """ @spec build_get_next_request(oid() | [oid()], non_neg_integer()) :: pdu() def build_get_next_request(oid_or_oids, request_id), do: Builder.build_get_next_request(oid_or_oids, request_id) @doc """ Builds a GETBULK request PDU (SNMPv2c only). ## Parameters - `oid_list`: Single OID list or list of OID lists to request - `request_id`: Unique request identifier - `non_repeaters`: Number of non-repeating variables (default: 0) - `max_repetitions`: Maximum repetitions for repeating variables (default: 10) ## Examples iex> pdu = SnmpKit.SnmpLib.PDU.build_get_bulk_request([1, 3, 6, 1, 2, 1, 2, 2], 123, 0, 10) iex> pdu.type :get_bulk_request """ @spec build_get_bulk_request( oid(), non_neg_integer(), non_neg_integer(), non_neg_integer() ) :: pdu() def build_get_bulk_request(oid_list, request_id, non_repeaters \\ 0, max_repetitions \\ 30) do Builder.build_get_bulk_request(oid_list, request_id, non_repeaters, max_repetitions) end @doc """ Builds a SET request PDU. ## Parameters - `oid_list`: Single OID as list of integers - `type_value`: Tuple of `{type, value}` for the SET operation - `request_id`: Unique request identifier ## Examples iex> pdu = SnmpKit.SnmpLib.PDU.build_set_request([1, 3, 6, 1, 2, 1, 1, 5, 0], {:octet_string, "Test"}, 123) iex> pdu.type :set_request """ @spec build_set_request(oid(), {atom(), any()}, non_neg_integer()) :: pdu() def build_set_request(oid_list, type_value, request_id), do: Builder.build_set_request(oid_list, type_value, request_id) @doc """ Builds a response PDU. ## Parameters - `request_pdu`: Original request PDU to respond to - `varbinds`: List of variable bindings for the response - `error_status`: Error status code (default: 0 for no error) - `error_index`: Error index (default: 0) ## Examples iex> varbinds = [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "Linux server"}] iex> pdu = SnmpKit.SnmpLib.PDU.build_response(123, 0, 0, varbinds) iex> pdu.type :get_response """ @spec build_response(non_neg_integer(), error_status(), non_neg_integer(), [varbind()]) :: pdu() def build_response(request_id, error_status, error_index, varbinds \\ []) do Builder.build_response(request_id, error_status, error_index, varbinds) end @doc """ Builds an SNMP message with version, community, and PDU. ## Parameters - `pdu`: PDU structure to include in the message - `community`: Community string for authentication - `version`: SNMP version ## Examples iex> pdu = SnmpKit.SnmpLib.PDU.build_get_request([1, 3, 6, 1, 2, 1, 1, 1, 0], 123) iex> message = SnmpKit.SnmpLib.PDU.build_message(pdu, "public", :v2c) iex> message.version 1 """ @spec build_message(pdu(), binary(), Constants.snmp_version()) :: message() def build_message(pdu, community, version \\ :v1), do: Builder.build_message(pdu, community, version) @doc """ Validates a community string. ## Parameters - `encoded_message`: Encoded SNMP message - `expected_community`: Expected community string ## Examples iex> SnmpKit.SnmpLib.PDU.validate_community(<<>>, "public") {:error, :decode_failed} """ @spec validate_community(binary(), binary()) :: :ok | {:error, atom()} def validate_community(encoded_message, expected_community), do: Builder.validate_community(encoded_message, expected_community) @doc """ Creates an error response PDU. ## Parameters - `request_pdu`: Original request PDU - `error_status`: Error status atom or code - `error_index`: Index of the variable that caused the error ## Examples iex> request_pdu = %{type: :get_request, request_id: 123, error_status: 0, error_index: 0, varbinds: []} iex> error_pdu = SnmpKit.SnmpLib.PDU.create_error_response(request_pdu, :no_such_name, 1) iex> error_pdu.error_status :no_such_name """ @spec create_error_response(pdu(), error_status() | atom(), non_neg_integer()) :: pdu() def create_error_response(request_pdu, error_status, error_index) do Builder.create_error_response(request_pdu, error_status, error_index) end @doc """ Creates an error response PDU. ## Parameters - `request_pdu`: Original request PDU - `error_status`: Error status atom or code ## Examples iex> request_pdu = %{type: :get_request, request_id: 123, error_status: 0, error_index: 0, varbinds: []} iex> error_pdu = SnmpKit.SnmpLib.PDU.create_error_response(request_pdu, :no_such_name) iex> error_pdu.error_status :no_such_name """ @spec create_error_response(pdu(), error_status() | atom()) :: pdu() def create_error_response(request_pdu, error_status) do Builder.create_error_response(request_pdu, error_status, 0) end ## Public API - Validation @doc """ Validates a PDU structure. ## Examples iex> pdu = %{type: :get_request, request_id: 123, error_status: 0, error_index: 0, varbinds: []} iex> {:ok, ^pdu} = SnmpKit.SnmpLib.PDU.validate(pdu) """ @spec validate(pdu()) :: {:ok, pdu()} | {:error, atom()} def validate(pdu), do: Builder.validate(pdu) ## Public API - Utility Functions @doc """ Normalizes an OID to a list of integers. ## Examples iex> SnmpKit.SnmpLib.PDU.normalize_oid([1, 3, 6, 1, 2, 1, 1, 1, 0]) [1, 3, 6, 1, 2, 1, 1, 1, 0] """ @spec normalize_oid(oid() | binary()) :: oid() def normalize_oid(oid), do: Constants.normalize_oid(oid) @doc """ Normalizes an SNMP type atom. ## Examples iex> SnmpKit.SnmpLib.PDU.normalize_type(:string) :octet_string """ @spec normalize_type(atom()) :: snmp_type() def normalize_type(type), do: Constants.normalize_type(type) @doc """ Converts an error status atom to its numeric code. ## Examples iex> SnmpKit.SnmpLib.PDU.error_status_to_code(:no_such_name) 2 """ @spec error_status_to_code(atom()) :: non_neg_integer() def error_status_to_code(status), do: Constants.error_status_to_code(status) @doc """ Converts an error status code to its atom representation. ## Examples iex> SnmpKit.SnmpLib.PDU.error_status_to_atom(2) :no_such_name """ @spec error_status_to_atom(non_neg_integer()) :: atom() def error_status_to_atom(code), do: Constants.error_status_to_atom(code) end