Traps and Informs
Receive SNMP notifications from your network, and send your own, from a manager, a simulated device or your own agent.
Setup
Mix.install([
{:snmpkit, "~> 2.0"}
])
alias SnmpKit.{SNMP, Sim}
alias SnmpKit.SnmpSim.ProfileLoader
# A free UDP port on the loopback interface, so the notebook runs anywhere
free_port = fn ->
{:ok, socket} = :gen_udp.open(0, ip: {127, 0, 0, 1})
{:ok, port} = :inet.port(socket)
:gen_udp.close(socket)
port
end
IO.puts("Notification modules ready!")
Three kinds of notification
Polling asks the device; a notification lets the device speak first: a link went down, a fan failed, the box rebooted. SNMP has three kinds.
| Kind | Version | Acknowledged | Sent with |
|---|---|---|---|
| trap | SNMPv1 | no | SNMP.send_trap/4 with version: :v1 |
| trap | SNMPv2c | no | SNMP.send_trap/4 |
| inform | SNMPv2c | yes, the receiver answers | SNMP.send_inform/4 |
A trap is fire and forget: one datagram, and :ok means it was handed to
the socket. An inform is re-sent until the receiver acknowledges it, so
the sender learns whether it arrived. All three go to UDP port 162 by
default.
SnmpKit.Trap receives all three. The senders live in
SnmpKit.SnmpMgr.Notify, reachable as SnmpKit.SNMP.send_trap/4 and
SnmpKit.SNMP.send_inform/4.
Start a receiver
A receiver is a GenServer bound to a UDP port. Port 162 needs a
privileged process; port: 0 picks a free port, which port/1 reads
back. The simplest handler is a pid, which gets one
{:snmp_trap, notification} message per notification.
{:ok, receiver} =
SnmpKit.Trap.start_link(port: 0, bind_address: "127.0.0.1", handler: self())
trap_port = SnmpKit.Trap.port(receiver)
receiver_target = "127.0.0.1:#{trap_port}"
Send a trap
send_trap/4 takes the target ("host", "host:port" or
{host, port}), the trap OID, a list of {oid, type, value} varbinds
and options. OIDs may be lists, dotted strings or MIB names. The
sysUpTime.0 and snmpTrapOID.0 varbinds every SNMPv2 notification
starts with are added for you.
:ok = SNMP.send_trap(receiver_target, "linkDown", [{"ifIndex.3", :integer, 3}])
receive do
{:snmp_trap, notification} -> notification
after
2_000 -> :no_trap
end
What a notification looks like
Every handler gets the same map, whatever the version:
kind-:trapor:informversion-:v1or:v2ccommunity,source(sender address and port),received_attrap_oidandtrap_name(nilwhen the MIB does not know the OID)uptime- the sender'ssysUpTime, in centisecondsvarbinds- enriched maps, as everySnmpKit.SNMPcall returns; for v2c thesysUpTime.0andsnmpTrapOID.0varbinds are kept here as well as lifted intouptimeandtrap_oidrequest_id- v2c onlyagent_address,enterprise,generic_trap,specific_trap- the SNMPv1 trap fields (agent_addressfalls back to the sender for v2c)
Options on the sending side: community: (default from
SnmpKit.SnmpMgr.Config), uptime: (default: the VM's uptime, in
centiseconds), request_id: and version: (:v2c or :v1).
:ok =
SNMP.send_trap(receiver_target, "warmStart", [],
community: "ops",
uptime: 4242,
request_id: 77
)
receive do
{:snmp_trap, n} -> Map.take(n, [:kind, :version, :community, :trap_name, :uptime, :request_id])
after
2_000 -> :no_trap
end
SNMPv1 traps
An SNMPv1 trap PDU has no snmpTrapOID; it carries an enterprise OID, a
generic trap number (0 coldStart to 5 egpNeighborLoss, 6 for
enterprise-specific), a specific trap number, the agent's address and a
time stamp. SnmpKit maps between the two forms per RFC 3584: a standard
trap name becomes the matching generic trap, and the receiver derives
trap_oid back from the fields.
:ok =
SNMP.send_trap(receiver_target, "linkDown", [{"ifIndex.3", :integer, 3}],
version: :v1,
agent_addr: {10, 1, 2, 3}
)
receive do
{:snmp_trap, %{version: :v1} = n} ->
Map.take(n, [:trap_oid, :trap_name, :enterprise, :generic_trap, :specific_trap, :agent_address])
after
2_000 -> :no_trap
end
An enterprise-specific trap, enterprise.0.N in SNMPv2 form, becomes
generic trap 6 with specific trap N. Override any field with
enterprise:, generic_trap: or specific_trap: if a legacy receiver
wants something else.
disk_full = "1.3.6.1.4.1.99999.0.7"
:ok =
SNMP.send_trap(receiver_target, disk_full, [{"1.3.6.1.4.1.99999.1.0", :octet_string, "/var"}],
version: :v1
)
receive do
{:snmp_trap, %{version: :v1} = n} ->
Map.take(n, [:trap_oid, :trap_name, :enterprise, :generic_trap, :specific_trap])
after
2_000 -> :no_trap
end
Informs
An inform is acknowledged: the receiver answers with a Response PDU
carrying the same request id, and send_inform/4 returns :ok only when
that answer arrives. It waits timeout: (default 5000 ms) and re-sends
retries: times (default 1) before giving up.
:ok = SNMP.send_inform(receiver_target, "linkUp", [{"ifIndex.3", :integer, 3}])
receive do
{:snmp_trap, %{kind: :inform} = n} -> {n.kind, n.version, n.trap_name, n.request_id}
after
2_000 -> :no_inform
end
The receiver counts what it acknowledged:
SnmpKit.Trap.stats(receiver)
When nobody answers, the result is {:error, :timeout} after
timeout * (retries + 1) milliseconds:
nobody = "127.0.0.1:#{free_port.()}"
{elapsed_us, result} =
:timer.tc(fn -> SNMP.send_inform(nobody, "linkUp", [], timeout: 300, retries: 2) end)
{result, div(elapsed_us, 1000)}
Informs exist since SNMPv2c, so version: :v1 is refused:
SNMP.send_inform(receiver_target, "linkUp", [], version: :v1)
Handlers
A handler is a pid, a function of one argument, or {module, function, extra_args} with the notification prepended to extra_args. Function
and MFA handlers run in a fresh process per notification, so a slow
handler never blocks the socket, and one that raises is logged while the
receiver stays up.
me = self()
{:ok, fun_receiver} =
SnmpKit.Trap.start_link(
port: 0,
bind_address: "127.0.0.1",
handler: fn n -> send(me, {:alert, n.trap_name, n.source}) end
)
defmodule Alerts do
def handle(notification, pid, tag), do: send(pid, {tag, notification.trap_name})
end
{:ok, mfa_receiver} =
SnmpKit.Trap.start_link(
port: 0,
bind_address: "127.0.0.1",
handler: {Alerts, :handle, [me, :mfa]}
)
:ok = SNMP.send_trap("127.0.0.1:#{SnmpKit.Trap.port(fun_receiver)}", "warmStart")
:ok = SNMP.send_trap("127.0.0.1:#{SnmpKit.Trap.port(mfa_receiver)}", "coldStart")
from_fun =
receive do
{:alert, name, source} -> {name, source}
after
2_000 -> :nothing
end
from_mfa =
receive do
{:mfa, name} -> name
after
2_000 -> :nothing
end
{from_fun, from_mfa}
Filtering by community
communities: lists the community strings a receiver accepts; anything
else is dropped, counted under rejected_community and reported as a
[:snmpkit, :trap, :rejected] telemetry event. Undecodable packets and
SNMPv3 datagrams are counted the same way, under decode_errors and
unsupported.
{:ok, strict} =
SnmpKit.Trap.start_link(
port: 0,
bind_address: "127.0.0.1",
handler: self(),
communities: ["traps-only"]
)
strict_target = "127.0.0.1:#{SnmpKit.Trap.port(strict)}"
:ok = SNMP.send_trap(strict_target, "warmStart", [], community: "public")
:ok = SNMP.send_trap(strict_target, "warmStart", [], community: "traps-only")
accepted =
receive do
{:snmp_trap, n} -> n.community
after
2_000 -> :no_trap
end
{accepted, SnmpKit.Trap.stats(strict)}
Trap names and the MIB
The standard traps live under snmpTraps (1.3.6.1.6.3.1.1.5) and
snmpTrapOID is the object whose .0 instance carries the trap OID in
an SNMPv2 notification. All of them resolve through the MIB registry, in
both directions, which is where trap_name comes from.
for name <- ["coldStart", "warmStart", "linkDown", "linkUp", "authenticationFailure", "snmpTrapOID"] do
{:ok, oid} = SnmpKit.MIB.resolve(name)
IO.puts("#{String.pad_trailing(name, 22)} #{Enum.join(oid, ".")}")
end
SnmpKit.MIB.reverse_lookup([1, 3, 6, 1, 6, 3, 1, 1, 5, 1])
A vendor's traps get names the same way once its MIB is compiled and
loaded (see MIB management); until then they
are named by the nearest known ancestor, as enterprises.99999.0.7 was
above. A name nothing knows is refused before anything is sent:
SNMP.send_trap(receiver_target, "noSuchTrap")
From a simulated device
SnmpKit.Sim.send_trap/4 sends from a simulated device with the device's
own community and sysUpTime (a few centiseconds, for a device started a
moment ago); to: names the receiver.
{:ok, profile} = ProfileLoader.load_profile(:router)
{:ok, device} =
Sim.start_device(profile, port: free_port.(), bind_address: "127.0.0.1", community: "lab")
:ok = Sim.send_trap(device, "linkDown", [{"ifIndex.1", :integer, 1}], to: receiver_target)
receive do
{:snmp_trap, n} -> {n.community, n.uptime, n.trap_name}
after
2_000 -> :no_trap
end
From your own agent
SnmpKit.Agent.notify/4 sends to the agent's notify_targets: (or to
targets: in the call) with the agent's sysUpTime; inform: true
sends informs and waits for the acknowledgements. The
agent notebook shows the trap; here is the inform,
and what a failed target looks like.
{:ok, agent} =
SnmpKit.Agent.start_link(
port: 0,
bind_address: "127.0.0.1",
notify_targets: [{"127.0.0.1", trap_port}]
)
:ok = SnmpKit.Agent.notify(agent, "coldStart", [], inform: true)
receive do
{:snmp_trap, n} -> {n.kind, n.trap_name, n.uptime}
after
2_000 -> :no_inform
end
SnmpKit.Agent.notify(agent, "coldStart", [],
targets: [nobody],
inform: true,
timeout: 300,
retries: 0
)
SNMPv3
Notifications in SnmpKit are SNMPv1 and SNMPv2c. SnmpKit.Trap counts
SNMPv3 datagrams under unsupported and drops them, and the senders'
version: option takes :v1 or :v2c. Devices that speak v3 for
polling almost always allow a v2c trap destination alongside, which is
how they are usually configured for a receiver like this one.
In production
Put the receiver in your supervision tree. Binding port 162 needs root or
a capability such as setcap cap_net_bind_service=+ep on the BEAM
binary; a high port with a firewall redirect works too.
children = [
{SnmpKit.Trap,
port: 162,
bind_address: "0.0.0.0",
communities: ["public"],
handler: {Alerts, :handle, [self(), :production]}}
]
children
Each accepted notification emits [:snmpkit, :trap, :received] with its
kind, version, trap OID and name, community and source; rejected ones emit
[:snmpkit, :trap, :rejected] with the reason.
Clean up
for r <- [receiver, fun_receiver, mfa_receiver, strict], do: SnmpKit.Trap.stop(r)
SnmpKit.Agent.stop(agent)
SnmpKit.SnmpSim.stop_device(device)
Summary
SnmpKit.Trap.start_link/1receives SNMPv1 traps, SNMPv2c traps and informs on one port and hands each to a pid, a function or an MFA; informs are acknowledged for you- Every handler gets the same map:
kind,version,trap_oid,trap_name,uptime, enrichedvarbinds, plus the v1 fields SNMP.send_trap/4sends v2c (or v1) traps;SNMP.send_inform/4waits for the acknowledgement withtimeout:andretries:and returns{:error, :timeout}when none comesSnmpKit.Sim.send_trap/4andSnmpKit.Agent.notify/4send with the device's or agent's own community and uptime- Trap OIDs and varbind OIDs may be MIB names, and
trap_nameis resolved through the same registry - Everything here is v1/v2c; SNMPv3 notifications are dropped
Next Steps
- Your Own SNMP Agent - The agent whose
notify/4you just used - Telemetry and Rates - The
:telemetryevents receivers emit - Device Simulation - Devices to send traps from