towerops/lib/snmpkit/snmp_lib/pdu.ex
2026-01-23 12:52:17 -06:00

414 lines
12 KiB
Elixir

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..5,
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> :ok = SnmpKit.SnmpLib.PDU.validate_community(encoded_msg, "public")
"""
@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
2
"""
@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
2
"""
@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