///|
/// SNMP v1 standard UDP port.
pub let snmp_port : Int = 161
///|
/// SNMP v1 PDU types (ASN.1 constructed, context-specific).
pub let pdu_type_get : Byte = b'\xA0'
///|
pub let pdu_type_get_next : Byte = b'\xA1'
///|
pub let pdu_type_response : Byte = b'\xA2'
///|
pub let pdu_type_set : Byte = b'\xA3'
///|
/// ASN.1 tag constants.
pub let asn1_sequence : Byte = b'\x30'
///|
pub let asn1_integer : Byte = b'\x02'
///|
pub let asn1_octet_string : Byte = b'\x04'
///|
pub let asn1_null : Byte = b'\x05'
///|
pub let asn1_oid : Byte = b'\x06'
///|
/// Well-known PROFINET SNMP OIDs.
pub let oid_sys_descr : String = "1.3.6.1.2.1.1.1.0"
///|
pub let oid_sys_name : String = "1.3.6.1.2.1.1.5.0"
///|
pub let oid_sys_contact : String = "1.3.6.1.2.1.1.4.0"
///|
pub let oid_sys_location : String = "1.3.6.1.2.1.1.6.0"
///|
pub let oid_if_number : String = "1.3.6.1.2.1.2.1.0"
///|
/// LLDP MIB OIDs.
pub let oid_lldp_rem_sys_name : String = "1.0.8802.1.1.2.1.4.1.1.9"
///|
pub let oid_lldp_rem_port_id : String = "1.0.8802.1.1.2.1.4.1.1.7"
///|
/// SNMP v1 message model.
pub(all) struct SnmpMessage {
version : Int
community : String
pdu_type : Byte
request_id : Int
error_status : Int
error_index : Int
varbinds : Array[VarBind]
} derive(Eq, Debug)
///|
/// SNMP variable binding.
pub(all) struct VarBind {
oid : String
value : SnmpValue
} derive(Eq, Debug)
///|
/// SNMP value types (simplified).
pub(all) enum SnmpValue {
Null
IntVal(Int)
OctetString(Bytes)
OidVal(String)
} derive(Eq, Debug)
///|
struct Asn1Tlv {
tag : Byte
value_start : Int
value_end : Int
} derive(Eq, Debug)
///|
/// Encode OID string to ASN.1 bytes.
pub fn encode_oid(oid_str : String) -> Bytes {
let parts = oid_str.split(".").to_array()
let output : Array[Byte] = []
if parts.length() >= 2 {
let first = parse_oid_part(parts[0].to_owned())
let second = parse_oid_part(parts[1].to_owned())
output.push((first * 40 + second).to_byte())
for i = 2; i < parts.length(); i = i + 1 {
let value = parse_oid_part(parts[i].to_owned())
encode_oid_component(output, value)
}
}
Bytes::from_array(output)
}
///|
fn read_asn1_tlv(
data : Bytes,
offset : Int,
limit : Int,
) -> Asn1Tlv raise @frame.FrameError {
guard offset + 2 <= limit && limit <= data.length() else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let tag = data[offset]
let first_len = data[offset + 1].to_int()
let mut length = 0
let mut value_start = offset + 2
if (first_len & 0x80) == 0 {
length = first_len
} else {
let len_len = first_len & 0x7F
guard len_len > 0 && len_len <= 4 && value_start + len_len <= limit else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
for i = 0; i < len_len; i = i + 1 {
length = (length << 8) + data[value_start + i].to_int()
}
value_start = value_start + len_len
}
let value_end = value_start + length
guard value_end <= limit else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
Asn1Tlv::{ tag, value_start, value_end }
}
///|
fn parse_asn1_integer(data : Bytes, tlv : Asn1Tlv) -> Int {
let mut value = 0
for i = tlv.value_start; i < tlv.value_end; i = i + 1 {
value = (value << 8) + data[i].to_int()
}
value
}
///|
fn parse_asn1_string(data : Bytes, tlv : Asn1Tlv) -> String {
let mut s = ""
for i = tlv.value_start; i < tlv.value_end; i = i + 1 {
if data[i].to_int() == 0 {
break
}
s = s + data[i].to_int().unsafe_to_char().to_string()
}
s
}
///|
fn parse_asn1_oid(
data : Bytes,
tlv : Asn1Tlv,
) -> String raise @frame.FrameError {
guard tlv.value_start < tlv.value_end else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let first = data[tlv.value_start].to_int()
let first_component = if first >= 80 { 2 } else { first / 40 }
let second_component = if first >= 80 { first - 80 } else { first % 40 }
let components : Array[Int] = [first_component, second_component]
let mut component = 0
let mut complete = true
for i = tlv.value_start + 1; i < tlv.value_end; i = i + 1 {
let byte = data[i].to_int()
component = (component << 7) | (byte & 0x7F)
complete = (byte & 0x80) == 0
if complete {
components.push(component)
component = 0
}
}
guard complete else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let parts : Array[String] = []
for component in components {
parts.push(component.to_string())
}
parts.join(".")
}
///|
fn parse_asn1_value(
data : Bytes,
tlv : Asn1Tlv,
) -> SnmpValue raise @frame.FrameError {
if tlv.tag == asn1_null {
Null
} else if tlv.tag == asn1_integer {
IntVal(parse_asn1_integer(data, tlv))
} else if tlv.tag == asn1_octet_string {
OctetString(data[tlv.value_start:tlv.value_end].to_owned())
} else if tlv.tag == asn1_oid {
OidVal(parse_asn1_oid(data, tlv))
} else {
OctetString(data[tlv.value_start:tlv.value_end].to_owned())
}
}
///|
/// Parse an SNMP v1 message from BER bytes.
pub fn parse_snmp_message(data : Bytes) -> SnmpMessage raise @frame.FrameError {
let top = read_asn1_tlv(data, 0, data.length())
guard top.tag == asn1_sequence && top.value_end == data.length() else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let version_tlv = read_asn1_tlv(data, top.value_start, top.value_end)
guard version_tlv.tag == asn1_integer else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let community_tlv = read_asn1_tlv(data, version_tlv.value_end, top.value_end)
guard community_tlv.tag == asn1_octet_string else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let pdu_tlv = read_asn1_tlv(data, community_tlv.value_end, top.value_end)
let request_tlv = read_asn1_tlv(data, pdu_tlv.value_start, pdu_tlv.value_end)
guard request_tlv.tag == asn1_integer else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let error_status_tlv = read_asn1_tlv(
data,
request_tlv.value_end,
pdu_tlv.value_end,
)
guard error_status_tlv.tag == asn1_integer else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let error_index_tlv = read_asn1_tlv(
data,
error_status_tlv.value_end,
pdu_tlv.value_end,
)
guard error_index_tlv.tag == asn1_integer else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let varbind_list_tlv = read_asn1_tlv(
data,
error_index_tlv.value_end,
pdu_tlv.value_end,
)
guard varbind_list_tlv.tag == asn1_sequence else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let varbinds : Array[VarBind] = []
let mut pos = varbind_list_tlv.value_start
while pos < varbind_list_tlv.value_end {
let varbind_tlv = read_asn1_tlv(data, pos, varbind_list_tlv.value_end)
guard varbind_tlv.tag == asn1_sequence else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let oid_tlv = read_asn1_tlv(
data,
varbind_tlv.value_start,
varbind_tlv.value_end,
)
guard oid_tlv.tag == asn1_oid else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let value_tlv = read_asn1_tlv(
data,
oid_tlv.value_end,
varbind_tlv.value_end,
)
let oid = parse_asn1_oid(data, oid_tlv)
let value = parse_asn1_value(data, value_tlv)
varbinds.push(VarBind::{ oid, value })
pos = varbind_tlv.value_end
}
SnmpMessage::{
version: parse_asn1_integer(data, version_tlv),
community: parse_asn1_string(data, community_tlv),
pdu_type: pdu_tlv.tag,
request_id: parse_asn1_integer(data, request_tlv),
error_status: parse_asn1_integer(data, error_status_tlv),
error_index: parse_asn1_integer(data, error_index_tlv),
varbinds,
}
}
///|
fn parse_oid_part(s : String) -> Int {
let mut value = 0
for c in s {
if c >= '0' && c <= '9' {
value = value * 10 + (c.to_int() - '0'.to_int())
}
}
value
}
///|
fn encode_oid_component(output : Array[Byte], value : Int) -> Unit {
if value < 128 {
output.push(value.to_byte())
} else if value < 16384 {
output.push(((value >> 7) | 0x80).to_byte())
output.push((value & 0x7F).to_byte())
} else {
output.push(((value >> 14) | 0x80).to_byte())
output.push((((value >> 7) & 0x7F) | 0x80).to_byte())
output.push((value & 0x7F).to_byte())
}
}
///|
fn encode_asn1_length(output : Array[Byte], length : Int) -> Unit {
if length < 128 {
output.push(length.to_byte())
} else if length < 256 {
output.push(b'\x81')
output.push(length.to_byte())
} else {
output.push(b'\x82')
output.push(((length >> 8) & 0xFF).to_byte())
output.push((length & 0xFF).to_byte())
}
}
///|
fn encode_asn1_integer(output : Array[Byte], value : Int) -> Unit {
output.push(asn1_integer)
if value == 0 {
output.push(b'\x01')
output.push(b'\x00')
} else if value < 128 {
output.push(b'\x01')
output.push(value.to_byte())
} else if value < 32768 {
output.push(b'\x02')
output.push(((value >> 8) & 0xFF).to_byte())
output.push((value & 0xFF).to_byte())
} else {
output.push(b'\x04')
output.push(((value >> 24) & 0xFF).to_byte())
output.push(((value >> 16) & 0xFF).to_byte())
output.push(((value >> 8) & 0xFF).to_byte())
output.push((value & 0xFF).to_byte())
}
}
///|
fn encode_asn1_string(output : Array[Byte], s : String) -> Unit {
output.push(asn1_octet_string)
let bytes = s.to_array().map(fn(c) { c.to_int().to_byte() })
encode_asn1_length(output, bytes.length())
for b in bytes {
output.push(b)
}
}
///|
fn encode_asn1_oid_tlv(output : Array[Byte], oid_bytes : Bytes) -> Unit {
output.push(asn1_oid)
encode_asn1_length(output, oid_bytes.length())
for b in oid_bytes {
output.push(b)
}
}
///|
fn encode_snmp_value(output : Array[Byte], value : SnmpValue) -> Unit {
match value {
Null => {
output.push(asn1_null)
output.push(b'\x00')
}
IntVal(v) => encode_asn1_integer(output, v)
OctetString(data) => {
output.push(asn1_octet_string)
encode_asn1_length(output, data.length())
for b in data {
output.push(b)
}
}
OidVal(oid) => {
let oid_bytes = encode_oid(oid)
encode_asn1_oid_tlv(output, oid_bytes)
}
}
}
///|
fn encode_varbind(output : Array[Byte], vb : VarBind) -> Unit {
let inner : Array[Byte] = []
let oid_bytes = encode_oid(vb.oid)
encode_asn1_oid_tlv(inner, oid_bytes)
encode_snmp_value(inner, vb.value)
output.push(asn1_sequence)
encode_asn1_length(output, inner.length())
for b in inner {
output.push(b)
}
}
///|
/// Encode an SNMP v1 message.
pub fn encode_snmp_message(msg : SnmpMessage) -> Bytes {
// Build PDU body
let pdu_body : Array[Byte] = []
encode_asn1_integer(pdu_body, msg.request_id)
encode_asn1_integer(pdu_body, msg.error_status)
encode_asn1_integer(pdu_body, msg.error_index)
// varbind list
let varbinds_body : Array[Byte] = []
for vb in msg.varbinds {
encode_varbind(varbinds_body, vb)
}
pdu_body.push(asn1_sequence)
encode_asn1_length(pdu_body, varbinds_body.length())
for b in varbinds_body {
pdu_body.push(b)
}
// Build message body
let msg_body : Array[Byte] = []
encode_asn1_integer(msg_body, msg.version)
encode_asn1_string(msg_body, msg.community)
msg_body.push(msg.pdu_type)
encode_asn1_length(msg_body, pdu_body.length())
for b in pdu_body {
msg_body.push(b)
}
// Wrap in SEQUENCE
let result : Array[Byte] = []
result.push(asn1_sequence)
encode_asn1_length(result, msg_body.length())
for b in msg_body {
result.push(b)
}
Bytes::from_array(result)
}
///|
fn is_printable_ascii(data : Bytes) -> Bool {
for b in data {
let value = b.to_int()
if value < 0x20 || value > 0x7E {
return false
}
}
true
}
///|
fn ascii_bytes_to_string(data : Bytes) -> String {
let mut s = ""
for b in data {
if b.to_int() == 0 {
break
}
s = s + b.to_int().unsafe_to_char().to_string()
}
s
}
///|
/// Format a simplified SNMP value for diagnostics.
pub fn format_snmp_value(value : SnmpValue) -> String {
match value {
Null => "null"
IntVal(v) => v.to_string()
OctetString(data) =>
if is_printable_ascii(data) {
"\"" + ascii_bytes_to_string(data) + "\""
} else {
"0x" + @frame.bytes_to_hex(data, separator="")
}
OidVal(oid) => oid
}
}
///|
/// Build a simple SNMP GET request for a single OID.
pub fn build_get_request(
community : String,
request_id : Int,
oid : String,
) -> SnmpMessage {
SnmpMessage::{
version: 0,
community,
pdu_type: pdu_type_get,
request_id,
error_status: 0,
error_index: 0,
varbinds: [VarBind::{ oid, value: Null }],
}
}
///|
/// Build a simple SNMP GET-NEXT request for a single OID.
pub fn build_get_next_request(
community : String,
request_id : Int,
oid : String,
) -> SnmpMessage {
SnmpMessage::{
version: 0,
community,
pdu_type: pdu_type_get_next,
request_id,
error_status: 0,
error_index: 0,
varbinds: [VarBind::{ oid, value: Null }],
}
}
///|
/// Build a simple SNMP SET request.
pub fn build_set_request(
community : String,
request_id : Int,
oid : String,
value : SnmpValue,
) -> SnmpMessage {
SnmpMessage::{
version: 0,
community,
pdu_type: pdu_type_set,
request_id,
error_status: 0,
error_index: 0,
varbinds: [VarBind::{ oid, value }],
}
}
///|
/// Format SNMP message summary.
pub fn format_snmp_message(msg : SnmpMessage) -> String {
let pdu_label = match msg.pdu_type {
b'\xA0' => "GET"
b'\xA1' => "GET-NEXT"
b'\xA2' => "RESPONSE"
b'\xA3' => "SET"
_ => "UNKNOWN"
}
let lines : Array[String] = []
lines.push("version=SNMPv" + (msg.version + 1).to_string())
lines.push("community=" + msg.community)
lines.push("pdu_type=" + pdu_label)
lines.push("request_id=" + msg.request_id.to_string())
lines.push("error_status=" + msg.error_status.to_string())
lines.push("varbind_count=" + msg.varbinds.length().to_string())
for vb in msg.varbinds {
lines.push(" oid=" + vb.oid + " value=" + format_snmp_value(vb.value))
}
lines.join("\n")
}