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 error_injector.ex
Raw

lib/snmpkit/snmp_sim/error_injector.ex

defmodule SnmpKit.SnmpSim.ErrorInjector do
@moduledoc """
Inject realistic error conditions for comprehensive testing.
Supports timeouts, packet loss, malformed responses, and device failures
for testing SNMP polling systems under realistic network conditions.
## Features
- Network timeouts with configurable probability and duration
- Packet loss simulation with burst patterns
- SNMP protocol errors (noSuchName, genErr, tooBig)
- Malformed response corruption for robustness testing
- Device failure and reboot simulation
- Statistical tracking of all injected errors
## Usage
# Inject timeout condition
SnmpKit.SnmpSim.ErrorInjector.inject_timeout(device_pid, probability: 0.1, duration: 5000)
# Simulate packet loss
SnmpKit.SnmpSim.ErrorInjector.inject_packet_loss(device_pid, loss_rate: 0.05)
# Generate SNMP errors
SnmpKit.SnmpSim.ErrorInjector.inject_snmp_error(device_pid, :noSuchName, ["1.3.6.1.2.1.2.2.1.99"])
"""
use GenServer
require Logger
@type error_type :: :timeout | :packet_loss | :snmp_error | :malformed | :device_failure
@type error_config :: %{
probability: float(),
duration_ms: integer(),
burst_patterns: boolean(),
target_oids: list(String.t()) | :all,
error_details: map()
}
defstruct [
:device_pid,
:device_port,
# Map of active error conditions
:error_conditions,
# Statistics tracking
:error_statistics,
# Current burst error state
:burst_state,
# Scheduled error timers
:schedule_timers
]
## Public API
@doc """
Start error injection monitoring for a device.
"""
@spec start_link(pid(), integer()) :: {:ok, pid()} | {:error, term()}
def start_link(device_pid, device_port) do
GenServer.start_link(__MODULE__, {device_pid, device_port}, [])
end
@doc """
Inject timeout conditions with specified probability and duration.
## Options
- `probability`: Float 0.0-1.0, chance each request times out
- `duration_ms`: Timeout duration in milliseconds
- `burst_probability`: Chance of timeout bursts (default: 0.1)
- `burst_duration_ms`: Duration of timeout bursts (default: 10000)
## Examples
# 10% chance of 5-second timeouts
ErrorInjector.inject_timeout(device, probability: 0.1, duration_ms: 5000)
# Burst timeouts - 20% of requests timeout for 30 seconds when burst occurs
ErrorInjector.inject_timeout(device,
probability: 0.2,
duration_ms: 30000,
burst_probability: 0.05,
burst_duration_ms: 60000
)
"""
@spec inject_timeout(pid(), keyword()) :: :ok | {:error, term()}
def inject_timeout(injector_pid, opts \\ []) do
config = %{
type: :timeout,
probability: Keyword.get(opts, :probability, 0.1),
duration_ms: Keyword.get(opts, :duration_ms, 5000),
burst_probability: Keyword.get(opts, :burst_probability, 0.1),
burst_duration_ms: Keyword.get(opts, :burst_duration_ms, 10000),
target_oids: Keyword.get(opts, :target_oids, :all)
}
GenServer.call(injector_pid, {:inject_error, config})
end
@doc """
Inject packet loss with configurable loss rates and patterns.
## Options
- `loss_rate`: Float 0.0-1.0, percentage of packets to drop
- `burst_loss`: Enable burst loss patterns (default: false)
- `burst_size`: Number of consecutive packets to drop in burst (default: 5)
- `recovery_time_ms`: Time between bursts (default: 30000)
## Examples
# 5% random packet loss
ErrorInjector.inject_packet_loss(device, loss_rate: 0.05)
# Burst packet loss - lose 10 consecutive packets occasionally
ErrorInjector.inject_packet_loss(device,
loss_rate: 0.02,
burst_loss: true,
burst_size: 10,
recovery_time_ms: 60000
)
"""
@spec inject_packet_loss(pid(), keyword()) :: :ok | {:error, term()}
def inject_packet_loss(injector_pid, opts \\ []) do
config = %{
type: :packet_loss,
loss_rate: Keyword.get(opts, :loss_rate, 0.05),
burst_loss: Keyword.get(opts, :burst_loss, false),
burst_size: Keyword.get(opts, :burst_size, 5),
recovery_time_ms: Keyword.get(opts, :recovery_time_ms, 30000),
target_oids: Keyword.get(opts, :target_oids, :all)
}
GenServer.call(injector_pid, {:inject_error, config})
end
@doc """
Inject SNMP protocol errors for specific OIDs or patterns.
## Error Types
- `:noSuchName` - OID does not exist
- `:genErr` - General error
- `:tooBig` - Response too large for UDP packet
- `:badValue` - Invalid value in SET request
- `:readOnly` - Attempt to SET read-only variable
## Examples
# Generate noSuchName errors for specific OIDs
ErrorInjector.inject_snmp_error(device, :noSuchName,
target_oids: ["1.3.6.1.2.1.2.2.1.99"],
probability: 1.0
)
# Random genErr responses
ErrorInjector.inject_snmp_error(device, :genErr,
probability: 0.05,
target_oids: :all
)
"""
@spec inject_snmp_error(pid(), atom(), keyword()) :: :ok | {:error, term()}
def inject_snmp_error(injector_pid, error_type, opts \\ []) do
config = %{
type: :snmp_error,
snmp_error_type: error_type,
probability: Keyword.get(opts, :probability, 0.1),
target_oids: Keyword.get(opts, :target_oids, :all),
error_index: Keyword.get(opts, :error_index, 1)
}
GenServer.call(injector_pid, {:inject_error, config})
end
@doc """
Inject malformed response packets to test client robustness.
## Corruption Types
- `:truncated` - Cut off response packets
- `:invalid_ber` - Corrupt BER/DER encoding
- `:wrong_community` - Incorrect community string
- `:invalid_pdu_type` - Invalid PDU type field
- `:corrupted_varbinds` - Corrupt variable bindings
## Examples
# Randomly truncate 2% of responses
ErrorInjector.inject_malformed_response(device, :truncated,
probability: 0.02,
corruption_severity: 0.3
)
# Corrupt BER encoding occasionally
ErrorInjector.inject_malformed_response(device, :invalid_ber,
probability: 0.01
)
"""
@spec inject_malformed_response(pid(), atom(), keyword()) :: :ok | {:error, term()}
def inject_malformed_response(injector_pid, corruption_type, opts \\ []) do
config = %{
type: :malformed,
corruption_type: corruption_type,
probability: Keyword.get(opts, :probability, 0.05),
corruption_severity: Keyword.get(opts, :corruption_severity, 0.5),
target_oids: Keyword.get(opts, :target_oids, :all)
}
GenServer.call(injector_pid, {:inject_error, config})
end
@doc """
Simulate device reboot or failure scenarios.
## Failure Types
- `:reboot` - Device becomes unreachable then recovers
- `:power_failure` - Complete device failure
- `:network_disconnect` - Network connectivity lost
- `:firmware_crash` - Device crash with recovery
- `:overload` - Device overloaded, slow responses
## Examples
# Simulate device reboot (30 seconds downtime)
ErrorInjector.simulate_device_failure(device, :reboot,
duration_ms: 30000,
recovery_behavior: :reset_counters
)
# Network disconnect with gradual recovery
ErrorInjector.simulate_device_failure(device, :network_disconnect,
duration_ms: 60000,
recovery_behavior: :gradual
)
"""
@spec simulate_device_failure(pid(), atom(), keyword()) :: :ok | {:error, term()}
def simulate_device_failure(injector_pid, failure_type, opts \\ []) do
config = %{
type: :device_failure,
failure_type: failure_type,
duration_ms: Keyword.get(opts, :duration_ms, 3000),
recovery_behavior: Keyword.get(opts, :recovery_behavior, :normal),
failure_probability: Keyword.get(opts, :failure_probability, 1.0)
}
GenServer.call(injector_pid, {:inject_error, config})
end
@doc """
Get statistics for all injected errors.
"""
@spec get_error_statistics(pid()) :: map()
def get_error_statistics(injector_pid) do
GenServer.call(injector_pid, :get_statistics)
end
@doc """
Clear all error conditions and reset device to normal operation.
"""
@spec clear_all_errors(pid()) :: :ok
def clear_all_errors(injector_pid) do
GenServer.call(injector_pid, :clear_all_errors)
end
@doc """
Remove specific error condition.
"""
@spec remove_error_condition(pid(), error_type()) :: :ok
def remove_error_condition(injector_pid, error_type) do
GenServer.call(injector_pid, {:remove_error, error_type})
end
## GenServer Implementation
@impl true
def init({device_pid, device_port}) do
Logger.info("Starting error injector for device on port #{device_port}")
state = %__MODULE__{
device_pid: device_pid,
device_port: device_port,
error_conditions: %{},
error_statistics: initialize_statistics(),
burst_state: %{},
schedule_timers: %{}
}
{:ok, state}
end
@impl true
def handle_call({:inject_error, config}, _from, state) do
Logger.debug("Injecting #{config.type} error for device #{state.device_port}")
# Add error condition to device
error_id = generate_error_id(config.type)
updated_conditions = Map.put(state.error_conditions, error_id, config)
# Start scheduled timers if needed
updated_timers = maybe_schedule_error_timers(config, error_id, state.schedule_timers)
# Update statistics
updated_stats = update_injection_statistics(state.error_statistics, config.type)
# Apply error condition to device
case apply_error_condition(state.device_pid, config) do
:ok ->
new_state = %{
state
| error_conditions: updated_conditions,
schedule_timers: updated_timers,
error_statistics: updated_stats
}
{:reply, :ok, new_state}
{:error, reason} ->
Logger.error("Failed to inject error: #{inspect(reason)}")
{:reply, {:error, reason}, state}
end
end
@impl true
def handle_call(:get_statistics, _from, state) do
{:reply, state.error_statistics, state}
end
@impl true
def handle_call(:clear_all_errors, _from, state) do
Logger.info("Clearing all error conditions for device #{state.device_port}")
# Clear all timers
Enum.each(state.schedule_timers, fn {_id, timer_ref} ->
Process.cancel_timer(timer_ref)
end)
# Reset device to normal operation
:ok = clear_device_errors(state.device_pid)
cleared_state = %{state | error_conditions: %{}, schedule_timers: %{}, burst_state: %{}}
{:reply, :ok, cleared_state}
end
@impl true
def handle_call({:remove_error, error_type}, _from, state) do
# Find and remove error conditions of specified type
{removed_conditions, remaining_conditions} =
Enum.split_with(state.error_conditions, fn {_id, config} ->
config.type == error_type
end)
# Cancel associated timers
updated_timers =
Enum.reduce(removed_conditions, state.schedule_timers, fn {error_id, _config}, timers ->
case Map.get(timers, error_id) do
nil ->
timers
timer_ref ->
Process.cancel_timer(timer_ref)
Map.delete(timers, error_id)
end
end)
new_state = %{
state
| error_conditions: Map.new(remaining_conditions),
schedule_timers: updated_timers
}
{:reply, :ok, new_state}
end
@impl true
def handle_info({:scheduled_error, error_id, action}, state) do
case Map.get(state.error_conditions, error_id) do
nil ->
# Error condition was removed
{:noreply, state}
config ->
case action do
:activate_burst ->
handle_burst_activation(error_id, config, state)
:deactivate_burst ->
handle_burst_deactivation(error_id, config, state)
:device_recovery ->
handle_device_recovery(error_id, config, state)
_ ->
Logger.warning("Unknown scheduled error action: #{action}")
{:noreply, state}
end
end
end
@impl true
def handle_info(msg, state) do
Logger.debug("Unexpected message in ErrorInjector: #{inspect(msg)}")
{:noreply, state}
end
## Private Functions
defp initialize_statistics do
%{
total_injections: 0,
injections_by_type: %{
timeout: 0,
packet_loss: 0,
snmp_error: 0,
malformed: 0,
device_failure: 0
},
errors_triggered: %{},
burst_events: 0,
device_failures: 0,
last_injection: nil,
start_time: DateTime.utc_now()
}
end
defp generate_error_id(type) do
timestamp = :os.system_time(:microsecond)
"#{type}_#{timestamp}_#{:rand.uniform(1000)}"
end
defp update_injection_statistics(stats, error_type) do
%{
stats
| total_injections: stats.total_injections + 1,
injections_by_type: Map.update(stats.injections_by_type, error_type, 1, &(&1 + 1)),
last_injection: DateTime.utc_now()
}
end
defp apply_error_condition(device_pid, config) do
case config.type do
:timeout ->
apply_timeout_condition(device_pid, config)
:packet_loss ->
apply_packet_loss_condition(device_pid, config)
:snmp_error ->
apply_snmp_error_condition(device_pid, config)
:malformed ->
apply_malformed_condition(device_pid, config)
:device_failure ->
apply_device_failure_condition(device_pid, config)
_ ->
{:error, {:unknown_error_type, config.type}}
end
end
defp apply_timeout_condition(device_pid, config) do
# Send timeout configuration to device
timeout_config = %{
probability: config.probability,
duration_ms: config.duration_ms,
target_oids: config.target_oids
}
send(device_pid, {:error_injection, :timeout, timeout_config})
:ok
end
defp apply_packet_loss_condition(device_pid, config) do
# Send packet loss configuration to device
loss_config = %{
loss_rate: config.loss_rate,
burst_loss: config[:burst_loss] || false,
burst_size: config[:burst_size] || 5,
target_oids: config.target_oids
}
send(device_pid, {:error_injection, :packet_loss, loss_config})
:ok
end
defp apply_snmp_error_condition(device_pid, config) do
# Send SNMP error configuration to device
snmp_config = %{
error_type: config.snmp_error_type,
probability: config.probability,
target_oids: config.target_oids,
error_index: config[:error_index] || 1
}
send(device_pid, {:error_injection, :snmp_error, snmp_config})
:ok
end
defp apply_malformed_condition(device_pid, config) do
# Send malformed response configuration to device
malformed_config = %{
corruption_type: config.corruption_type,
probability: config.probability,
corruption_severity: config[:corruption_severity] || 0.5,
target_oids: config.target_oids
}
send(device_pid, {:error_injection, :malformed, malformed_config})
:ok
end
defp apply_device_failure_condition(device_pid, config) do
# Send device failure configuration to device
failure_config = %{
failure_type: config.failure_type,
duration_ms: config.duration_ms,
recovery_behavior: config[:recovery_behavior] || :normal,
failure_probability: config[:failure_probability] || 1.0
}
send(device_pid, {:error_injection, :device_failure, failure_config})
:ok
end
defp clear_device_errors(device_pid) do
send(device_pid, {:error_injection, :clear_all})
:ok
end
defp maybe_schedule_error_timers(config, error_id, current_timers) do
case config.type do
:timeout ->
if Map.get(config, :burst_probability, 0) > 0 do
schedule_burst_timers(config, error_id, current_timers)
else
current_timers
end
:packet_loss ->
if Map.get(config, :burst_loss, false) do
schedule_burst_timers(config, error_id, current_timers)
else
current_timers
end
:device_failure ->
schedule_recovery_timer(config, error_id, current_timers)
_ ->
current_timers
end
end
defp schedule_burst_timers(config, error_id, timers) do
# Schedule burst activation
burst_interval = Map.get(config, :recovery_time_ms, 30000)
timer_ref =
Process.send_after(self(), {:scheduled_error, error_id, :activate_burst}, burst_interval)
Map.put(timers, error_id, timer_ref)
end
defp schedule_recovery_timer(config, error_id, timers) do
# Schedule device recovery
timer_ref =
Process.send_after(
self(),
{:scheduled_error, error_id, :device_recovery},
config.duration_ms
)
Map.put(timers, error_id, timer_ref)
end
defp handle_burst_activation(error_id, config, state) do
Logger.debug("Activating burst error for #{error_id}")
# Update burst state
updated_burst =
Map.put(state.burst_state, error_id, %{
active: true,
start_time: DateTime.utc_now(),
packets_affected: 0
})
# Schedule burst deactivation
burst_duration = Map.get(config, :burst_duration_ms, 10000)
timer_ref =
Process.send_after(self(), {:scheduled_error, error_id, :deactivate_burst}, burst_duration)
updated_timers = Map.put(state.schedule_timers, "#{error_id}_deactivate", timer_ref)
# Update statistics
updated_stats = %{
state.error_statistics
| burst_events: state.error_statistics.burst_events + 1
}
new_state = %{
state
| burst_state: updated_burst,
schedule_timers: updated_timers,
error_statistics: updated_stats
}
{:noreply, new_state}
end
defp handle_burst_deactivation(error_id, _config, state) do
Logger.debug("Deactivating burst error for #{error_id}")
# Remove burst state
updated_burst = Map.delete(state.burst_state, error_id)
# Remove deactivation timer
updated_timers = Map.delete(state.schedule_timers, "#{error_id}_deactivate")
new_state = %{state | burst_state: updated_burst, schedule_timers: updated_timers}
{:noreply, new_state}
end
defp handle_device_recovery(error_id, config, state) do
Logger.info("Device recovery from #{config.failure_type} failure")
# Send recovery message to device
recovery_config = %{
failure_type: config.failure_type,
recovery_behavior: config[:recovery_behavior] || :normal
}
send(state.device_pid, {:error_injection, :recovery, recovery_config})
# Remove error condition
updated_conditions = Map.delete(state.error_conditions, error_id)
updated_timers = Map.delete(state.schedule_timers, error_id)
# Update statistics
updated_stats = %{
state.error_statistics
| device_failures: state.error_statistics.device_failures + 1
}
new_state = %{
state
| error_conditions: updated_conditions,
schedule_timers: updated_timers,
error_statistics: updated_stats
}
{:noreply, new_state}
end
end