556 lines
16 KiB
Elixir
556 lines
16 KiB
Elixir
defmodule SnmpKit.SnmpLib.PDU.Decoder do
|
|
@moduledoc """
|
|
ASN.1 BER decoding functions for SNMP PDUs and messages.
|
|
|
|
This module handles the conversion of binary ASN.1 BER format to Elixir data structures
|
|
for SNMP protocol communication.
|
|
"""
|
|
|
|
import Bitwise
|
|
|
|
alias SnmpKit.SnmpLib.PDU.Constants
|
|
alias SnmpKit.SnmpLib.PDU.V3Encoder
|
|
|
|
@type message :: Constants.message()
|
|
@type pdu :: Constants.pdu()
|
|
|
|
# Import constants for decoding
|
|
@integer Constants.integer()
|
|
@octet_string Constants.octet_string()
|
|
@null Constants.null()
|
|
@object_identifier Constants.object_identifier()
|
|
@counter32 Constants.counter32()
|
|
@gauge32 Constants.gauge32()
|
|
@timeticks Constants.timeticks()
|
|
@counter64 Constants.counter64()
|
|
@ip_address Constants.ip_address()
|
|
@opaque_type Constants.opaque_type()
|
|
@no_such_object Constants.no_such_object()
|
|
@no_such_instance Constants.no_such_instance()
|
|
@end_of_mib_view Constants.end_of_mib_view()
|
|
|
|
@doc """
|
|
Decodes an SNMP message from binary format.
|
|
"""
|
|
@spec decode_message(binary()) :: {:ok, message()} | {:error, atom()}
|
|
def decode_message(data) when is_binary(data) do
|
|
# Check if this is a SNMPv3 message by looking at version
|
|
case peek_version(data) do
|
|
{:ok, 3} ->
|
|
# Delegate to SNMPv3 decoder
|
|
V3Encoder.decode_message(data, nil)
|
|
|
|
{:ok, _version} ->
|
|
# Use standard v1/v2c decoder
|
|
decode_snmp_message_comprehensive(data)
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
rescue
|
|
error -> {:error, {:decoding_error, error}}
|
|
catch
|
|
error -> {:error, {:decoding_error, error}}
|
|
end
|
|
|
|
def decode_message(_), do: {:error, :invalid_input}
|
|
|
|
@doc """
|
|
Decodes an SNMP message with security user (SNMPv3).
|
|
"""
|
|
@spec decode_message(binary(), map() | nil) :: {:ok, message()} | {:error, atom()}
|
|
def decode_message(data, user) when is_binary(data) do
|
|
case peek_version(data) do
|
|
{:ok, 3} ->
|
|
V3Encoder.decode_message(data, user)
|
|
|
|
{:ok, _version} ->
|
|
# v1/v2c messages don't use security users
|
|
decode_message(data)
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Decodes a PDU from binary format.
|
|
"""
|
|
@spec decode_pdu(binary()) :: {:ok, pdu()} | {:error, atom()}
|
|
def decode_pdu(data) when is_binary(data) do
|
|
case parse_pdu_comprehensive(data) do
|
|
{:ok, pdu} -> {:ok, pdu}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
rescue
|
|
error -> {:error, {:decoding_error, error}}
|
|
catch
|
|
error -> {:error, {:decoding_error, error}}
|
|
end
|
|
|
|
@doc """
|
|
Decodes an SNMP message from binary format (alias for decode_message/1).
|
|
"""
|
|
@spec decode(binary()) :: {:ok, message()} | {:error, atom()}
|
|
def decode(data) when is_binary(data) do
|
|
decode_message(data)
|
|
end
|
|
|
|
# Private helper to peek at version without full decoding
|
|
defp peek_version(<<0x30, _length, 0x02, _version_length, version, _rest::binary>>) do
|
|
{:ok, version}
|
|
end
|
|
|
|
defp peek_version(data) when is_binary(data) do
|
|
case parse_sequence(data) do
|
|
{:ok, {content, _remaining}} ->
|
|
case parse_integer(content) do
|
|
{:ok, {version, _rest}} -> {:ok, version}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp peek_version(_), do: {:error, :invalid_data}
|
|
|
|
@doc """
|
|
Alias for decode/1.
|
|
"""
|
|
@spec decode_snmp_packet(binary()) :: {:ok, message()} | {:error, atom()}
|
|
def decode_snmp_packet(data), do: decode(data)
|
|
|
|
## Private Implementation
|
|
|
|
# Comprehensive decoding implementation (from SnmpSim)
|
|
defp decode_snmp_message_comprehensive(<<0x30, rest::binary>>) do
|
|
with {:ok, {_content_length, content}} <- parse_ber_length(rest),
|
|
{:ok, {version, community, pdu_data}} <- parse_snmp_message_fields(content),
|
|
{:ok, pdu} <- parse_pdu_comprehensive(pdu_data) do
|
|
{:ok,
|
|
%{
|
|
version: version,
|
|
community: community,
|
|
pdu: pdu
|
|
}}
|
|
else
|
|
{:error, reason} when is_tuple(reason) ->
|
|
{:error, reason}
|
|
|
|
{:error, reason} ->
|
|
{:error, {:message_parse_error, reason}}
|
|
end
|
|
end
|
|
|
|
defp decode_snmp_message_comprehensive(_), do: {:error, :invalid_message_format}
|
|
|
|
defp parse_ber_length(<<length, rest::binary>>) when length < 128 do
|
|
if byte_size(rest) >= length do
|
|
content = binary_part(rest, 0, length)
|
|
{:ok, {length, content}}
|
|
else
|
|
{:error, :insufficient_data}
|
|
end
|
|
end
|
|
|
|
defp parse_ber_length(<<length_of_length, rest::binary>>) when length_of_length >= 128 do
|
|
num_length_bytes = length_of_length - 128
|
|
|
|
if num_length_bytes > 0 and num_length_bytes <= 4 and byte_size(rest) >= num_length_bytes do
|
|
<<length_bytes::binary-size(num_length_bytes), remaining::binary>> = rest
|
|
actual_length = :binary.decode_unsigned(length_bytes, :big)
|
|
|
|
if byte_size(remaining) >= actual_length do
|
|
content = binary_part(remaining, 0, actual_length)
|
|
{:ok, {actual_length, content}}
|
|
else
|
|
{:error, :insufficient_data}
|
|
end
|
|
else
|
|
{:error, :invalid_length_encoding}
|
|
end
|
|
end
|
|
|
|
defp parse_ber_length(_), do: {:error, :invalid_length_format}
|
|
|
|
defp parse_snmp_message_fields(data) do
|
|
with {:ok, {version, rest1}} <- parse_integer(data),
|
|
{:ok, {community, rest2}} <- parse_octet_string(rest1),
|
|
{:ok, pdu_data} <- {:ok, rest2} do
|
|
{:ok, {version, community, pdu_data}}
|
|
end
|
|
end
|
|
|
|
defp parse_integer(<<@integer, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_length, value_bytes, remaining}} ->
|
|
if byte_size(value_bytes) > 0 do
|
|
value = decode_integer_value(value_bytes)
|
|
{:ok, {value, remaining}}
|
|
else
|
|
{:error, :invalid_integer_length}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_integer(_), do: {:error, :invalid_integer}
|
|
|
|
defp parse_octet_string(<<@octet_string, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_length, value_bytes, remaining}} ->
|
|
{:ok, {value_bytes, remaining}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_octet_string(_), do: {:error, :invalid_octet_string}
|
|
|
|
defp parse_ber_length_and_remaining(<<length, rest::binary>>) when length < 128 do
|
|
if byte_size(rest) >= length do
|
|
content = binary_part(rest, 0, length)
|
|
remaining = binary_part(rest, length, byte_size(rest) - length)
|
|
{:ok, {length, content, remaining}}
|
|
else
|
|
{:error, :insufficient_data}
|
|
end
|
|
end
|
|
|
|
defp parse_ber_length_and_remaining(<<length_of_length, rest::binary>>) when length_of_length >= 128 do
|
|
num_length_bytes = length_of_length - 128
|
|
|
|
if num_length_bytes > 0 and num_length_bytes <= 4 and byte_size(rest) >= num_length_bytes do
|
|
<<length_bytes::binary-size(num_length_bytes), remaining_with_content::binary>> = rest
|
|
actual_length = :binary.decode_unsigned(length_bytes, :big)
|
|
|
|
if byte_size(remaining_with_content) >= actual_length do
|
|
content = binary_part(remaining_with_content, 0, actual_length)
|
|
|
|
remaining =
|
|
binary_part(
|
|
remaining_with_content,
|
|
actual_length,
|
|
byte_size(remaining_with_content) - actual_length
|
|
)
|
|
|
|
{:ok, {actual_length, content, remaining}}
|
|
else
|
|
{:error, :insufficient_data}
|
|
end
|
|
else
|
|
{:error, :invalid_length_encoding}
|
|
end
|
|
end
|
|
|
|
defp parse_ber_length_and_remaining(_), do: {:error, :invalid_length_format}
|
|
|
|
defp parse_pdu_comprehensive(<<tag, rest::binary>>) when tag in [0xA0, 0xA1, 0xA2, 0xA3, 0xA5] do
|
|
pdu_type = tag_to_pdu_type(tag)
|
|
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_length, pdu_content, _remaining}} ->
|
|
build_pdu_from_content(pdu_type, pdu_content)
|
|
|
|
{:error, _reason} ->
|
|
build_empty_pdu(pdu_type)
|
|
end
|
|
end
|
|
|
|
defp parse_pdu_comprehensive(_), do: {:error, :invalid_pdu}
|
|
|
|
defp tag_to_pdu_type(tag) do
|
|
case tag do
|
|
0xA0 -> :get_request
|
|
0xA1 -> :get_next_request
|
|
0xA2 -> :get_response
|
|
0xA3 -> :set_request
|
|
0xA5 -> :get_bulk_request
|
|
end
|
|
end
|
|
|
|
defp build_pdu_from_content(:get_bulk_request, pdu_content) do
|
|
case parse_bulk_pdu_fields(pdu_content) do
|
|
{:ok, {request_id, non_repeaters, max_repetitions, varbinds}} ->
|
|
{:ok,
|
|
%{
|
|
type: :get_bulk_request,
|
|
request_id: request_id,
|
|
non_repeaters: non_repeaters,
|
|
max_repetitions: max_repetitions,
|
|
varbinds: varbinds
|
|
}}
|
|
|
|
{:error, _reason} ->
|
|
{:ok, %{type: :get_bulk_request, varbinds: [], non_repeaters: 0, max_repetitions: 0}}
|
|
end
|
|
end
|
|
|
|
defp build_pdu_from_content(pdu_type, pdu_content) do
|
|
case parse_standard_pdu_fields(pdu_content) do
|
|
{:ok, {request_id, error_status, error_index, varbinds}} ->
|
|
{:ok,
|
|
%{
|
|
type: pdu_type,
|
|
request_id: request_id,
|
|
error_status: error_status,
|
|
error_index: error_index,
|
|
varbinds: varbinds
|
|
}}
|
|
|
|
{:error, _reason} ->
|
|
{:ok, %{type: pdu_type, varbinds: [], error_status: 0, error_index: 0}}
|
|
end
|
|
end
|
|
|
|
defp build_empty_pdu(:get_bulk_request) do
|
|
{:ok, %{type: :get_bulk_request, varbinds: [], non_repeaters: 0, max_repetitions: 0}}
|
|
end
|
|
|
|
defp build_empty_pdu(pdu_type) do
|
|
{:ok, %{type: pdu_type, varbinds: [], error_status: 0, error_index: 0}}
|
|
end
|
|
|
|
defp parse_standard_pdu_fields(data) do
|
|
with {:ok, {request_id, rest1}} <- parse_integer(data),
|
|
{:ok, {error_status, rest2}} <- parse_integer(rest1),
|
|
{:ok, {error_index, rest3}} <- parse_integer(rest2),
|
|
{:ok, varbinds} <- parse_varbinds(rest3) do
|
|
{:ok, {request_id, error_status, error_index, varbinds}}
|
|
end
|
|
end
|
|
|
|
defp parse_bulk_pdu_fields(data) do
|
|
with {:ok, {request_id, rest1}} <- parse_integer(data),
|
|
{:ok, {non_repeaters, rest2}} <- parse_integer(rest1),
|
|
{:ok, {max_repetitions, rest3}} <- parse_integer(rest2),
|
|
{:ok, varbinds} <- parse_varbinds(rest3) do
|
|
{:ok, {request_id, non_repeaters, max_repetitions, varbinds}}
|
|
end
|
|
end
|
|
|
|
defp parse_varbinds(data) do
|
|
case parse_sequence(data) do
|
|
{:ok, {varbind_data, _rest}} -> parse_varbind_list(varbind_data, [])
|
|
{:error, _} -> {:ok, []}
|
|
end
|
|
end
|
|
|
|
defp parse_sequence(<<0x30, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_length, data, remaining}} ->
|
|
{:ok, {data, remaining}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_sequence(_), do: {:error, :not_sequence}
|
|
|
|
defp parse_varbind_list(<<>>, acc), do: {:ok, Enum.reverse(acc)}
|
|
|
|
defp parse_varbind_list(data, acc) do
|
|
case parse_sequence(data) do
|
|
{:ok, {varbind_data, rest}} ->
|
|
case parse_single_varbind(varbind_data) do
|
|
{:ok, varbind} -> parse_varbind_list(rest, [varbind | acc])
|
|
{:error, _} -> parse_varbind_list(rest, acc)
|
|
end
|
|
|
|
{:error, _} ->
|
|
{:ok, Enum.reverse(acc)}
|
|
end
|
|
end
|
|
|
|
defp parse_single_varbind(data) do
|
|
with {:ok, {oid, rest1}} <- parse_oid(data),
|
|
{:ok, {type, value, _rest2}} <- parse_value_with_type(rest1) do
|
|
{:ok, {oid, type, value}}
|
|
else
|
|
_ -> {:error, :invalid_varbind}
|
|
end
|
|
end
|
|
|
|
defp parse_oid(<<@object_identifier, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, oid_data, rest2}} ->
|
|
case decode_oid_data(oid_data) do
|
|
{:ok, oid} -> {:ok, {oid, rest2}}
|
|
error -> error
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_oid(_), do: {:error, :invalid_oid}
|
|
|
|
defp decode_oid_data(<<first, rest::binary>>) do
|
|
first_subid = div(first, 40)
|
|
second_subid = rem(first, 40)
|
|
|
|
case decode_oid_subids(rest, [second_subid, first_subid]) do
|
|
{:ok, subids} -> {:ok, Enum.reverse(subids)}
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
defp decode_oid_data(_), do: {:error, :invalid_oid_data}
|
|
|
|
defp decode_oid_subids(<<>>, acc), do: {:ok, acc}
|
|
|
|
defp decode_oid_subids(data, acc) do
|
|
case decode_oid_subid(data, 0) do
|
|
{:ok, {subid, rest}} -> decode_oid_subids(rest, [subid | acc])
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
defp decode_oid_subid(<<byte, rest::binary>>, acc) do
|
|
new_acc = (acc <<< 7) + (byte &&& 0x7F)
|
|
|
|
if (byte &&& 0x80) == 0 do
|
|
{:ok, {new_acc, rest}}
|
|
else
|
|
decode_oid_subid(rest, new_acc)
|
|
end
|
|
end
|
|
|
|
defp decode_oid_subid(<<>>, _), do: {:error, :incomplete_oid}
|
|
|
|
defp parse_value_with_type(<<@octet_string, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value, rest2}} -> {:ok, {:octet_string, value, rest2}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@integer, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value_bytes, rest2}} ->
|
|
{:ok, {:integer, decode_integer_value(value_bytes), rest2}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@null, 0, rest::binary>>) do
|
|
{:ok, {:null, :null, rest}}
|
|
end
|
|
|
|
defp parse_value_with_type(<<@object_identifier, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, oid_data, rest2}} ->
|
|
case decode_oid_data(oid_data) do
|
|
{:ok, oid_list} -> {:ok, {:object_identifier, oid_list, rest2}}
|
|
{:error, _} -> {:error, :invalid_oid}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@counter32, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value, rest2}} -> {:ok, {:counter32, decode_unsigned_integer(value), rest2}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@gauge32, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value, rest2}} -> {:ok, {:gauge32, decode_unsigned_integer(value), rest2}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@timeticks, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value, rest2}} -> {:ok, {:timeticks, decode_unsigned_integer(value), rest2}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@counter64, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value, rest2}} -> {:ok, {:counter64, decode_counter64(value), rest2}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@ip_address, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value, rest2}} -> {:ok, {:ip_address, value, rest2}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@opaque_type, rest::binary>>) do
|
|
case parse_ber_length_and_remaining(rest) do
|
|
{:ok, {_len, value, rest2}} -> {:ok, {:opaque, value, rest2}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp parse_value_with_type(<<@no_such_object, 0, rest::binary>>) do
|
|
{:ok, {:no_such_object, nil, rest}}
|
|
end
|
|
|
|
defp parse_value_with_type(<<@no_such_instance, 0, rest::binary>>) do
|
|
{:ok, {:no_such_instance, nil, rest}}
|
|
end
|
|
|
|
defp parse_value_with_type(<<@end_of_mib_view, 0, rest::binary>>) do
|
|
{:ok, {:end_of_mib_view, nil, rest}}
|
|
end
|
|
|
|
defp parse_value_with_type(_), do: {:error, :invalid_value}
|
|
|
|
defp decode_integer_value(<<byte>>) when byte < 128, do: byte
|
|
defp decode_integer_value(<<byte>>) when byte >= 128, do: byte - 256
|
|
|
|
defp decode_integer_value(data) do
|
|
value = :binary.decode_unsigned(data, :big)
|
|
bit_size = byte_size(data) * 8
|
|
|
|
if value >= 1 <<< (bit_size - 1) do
|
|
value - (1 <<< bit_size)
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
defp decode_unsigned_integer(data) when byte_size(data) <= 4 do
|
|
:binary.decode_unsigned(data, :big)
|
|
end
|
|
|
|
defp decode_unsigned_integer(data) when byte_size(data) == 5 do
|
|
# Handle 5-byte case for large 32-bit unsigned values that require leading zero padding
|
|
case data do
|
|
<<0, rest::binary-size(4)>> ->
|
|
# Leading zero byte for unsigned representation, decode the remaining 4 bytes
|
|
:binary.decode_unsigned(rest, :big)
|
|
|
|
_ ->
|
|
# If first byte is not zero, this exceeds 32-bit range
|
|
0
|
|
end
|
|
end
|
|
|
|
defp decode_unsigned_integer(_), do: 0
|
|
|
|
defp decode_counter64(data) when byte_size(data) <= 8 and byte_size(data) > 0 do
|
|
:binary.decode_unsigned(data, :big)
|
|
end
|
|
|
|
defp decode_counter64(_), do: 0
|
|
end
|