Skip to content

Commit

Permalink
Basic SNMPv3 support
Browse files Browse the repository at this point in the history
  • Loading branch information
dvolodin7 committed Jan 12, 2024
1 parent 6851f86 commit 4933581
Show file tree
Hide file tree
Showing 22 changed files with 1,198 additions and 66 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

To see unreleased changes, please see the [CHANGELOG on the main branch guide](https://github.com/gufolabs/gufo_snmp/blob/main/CHANGELOG.md).

## [Unreleased]

### Added
* Basic SNMPv3 support (plaintext, no auth).
* Snmpd.engine_id property.
* Tests for all SNMP versions.

## 0.3.0 - 2024-01-10

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ async with Snmpd(), SnmpSession(addr="127.0.0.1", port=10161) as session:

* Clean async API.
* SNMP v1/v2c support.
* SNMP v3 support (plaintext, no auth).
* High-performance.
* Zero-copy BER parsing.
* Full Python typing support.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async with Snmpd(), SnmpSession(addr="127.0.0.1", port=10161) as session:

* Clean async API.
* SNMP v1/v2c support.
* SNMP v3 support (plaintext, no auth).
* High-performance.
* Zero-copy BER parsing.
* Full Python typing support.
Expand Down
4 changes: 4 additions & 0 deletions docs/standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Gufo SNMP implements and is guided by the following standards:
* [RFC-1442][RFC-1442]: Structure of Management Information for version 2 of the Simple Network Management Protocol (SNMPv2).
* [RFC-1905][RFC-1905]: Protocol Operations for Version 2 of the Simple Network Management Protocol (SNMPv2)
* [RFC-2578][RFC-2578]: Structure of Management Information Version 2 (SMIv2).
* [RFC-3411][RFC-3411]: An Architecture for Describing Simple Network Management Protocol (SNMP) Management Frameworks.
* [RFC-3412][RFC-3412]: Message Processing and Dispatching for the Simple Network Management Protocol (SNMP)

## ITU-T

Expand All @@ -25,6 +27,8 @@ Gufo SNMP implements and is guided by the following standards:
[RFC-1442]: https://datatracker.ietf.org/doc/html/rfc1442
[RFC-1905]: https://datatracker.ietf.org/doc/html/rfc1905
[RFC-2578]: https://datatracker.ietf.org/doc/html/rfc2578
[RFC-3411]: https://datatracker.ietf.org/doc/html/rfc3411
[RFC-3412]: https://datatracker.ietf.org/doc/html/rfc3412
[PEP8]: https://peps.python.org/pep-0008/
[PEP484]: https://peps.python.org/pep-0484/
[PEP561]: https://peps.python.org/pep-0561/
Expand Down
3 changes: 2 additions & 1 deletion src/ber/t_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ impl<'a> BerDecoder<'a> for SnmpOption<'a> {
return Err(Err::Failure(SnmpError::Incomplete));
}
let (tail, hdr) = BerHeader::from_ber(i)?;
if !hdr.constructed || hdr.class != BerClass::Context {
if !hdr.constructed || (hdr.class != BerClass::Context && hdr.class != BerClass::Universal)
{
return Err(Err::Failure(SnmpError::UnexpectedTag));
}
//
Expand Down
7 changes: 7 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ pub enum SnmpError {
WouldBlock,
/// Connection refused
ConnectionRefused,
/// Unknown Security Model
UnknownSecurityModel,
/// Authentication error
AuthenticationFailed,
}

impl From<nom::Err<SnmpError>> for SnmpError {
Expand Down Expand Up @@ -87,6 +91,7 @@ create_exception!(
PySnmpError,
"Requested OID is not found"
);
create_exception!(_fast, PySnmpAuthError, PySnmpError, "Authentication failed");

impl From<SnmpError> for PyErr {
fn from(value: SnmpError) -> PyErr {
Expand All @@ -110,6 +115,8 @@ impl From<SnmpError> for PyErr {
SnmpError::WouldBlock => PyBlockingIOError::new_err("blocked"),
SnmpError::SocketError(x) => PyOSError::new_err(x),
SnmpError::ConnectionRefused => PyTimeoutError::new_err("connection refused"),
SnmpError::UnknownSecurityModel => PySnmpDecodeError::new_err("unknown security model"),
SnmpError::AuthenticationFailed => PySnmpAuthError::new_err("authentication failed"),
}
}
}
31 changes: 31 additions & 0 deletions src/gufo/snmp/_fast.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ from .typing import ValueType
class SnmpError(Exception): ...
class SnmpEncodeError(SnmpError): ...
class SnmpDecodeError(SnmpError): ...
class SnmpAuthError(SnmpError): ... # v3 only
class NoSuchInstance(SnmpError): ...

class GetNextIter(object):
Expand Down Expand Up @@ -83,3 +84,33 @@ class SnmpV2cClientSocket(object):
def recv_getresponse_bulk(
self: "SnmpV2cClientSocket", iter_getnext: GetBulkIter
) -> List[Tuple[str, ValueType]]: ...

class SnmpV3ClientSocket(object):
def __init__(
self: "SnmpV3ClientSocket",
addr: str,
engine_id: bytes,
user_name: str,
tos: int,
send_buffer_size: int,
recv_buffer_size: int,
) -> None: ...
def get_fd(self: "SnmpV3ClientSocket") -> int: ...
def send_get(self: "SnmpV3ClientSocket", oid: str) -> None: ...
def send_get_many(self: "SnmpV3ClientSocket", oids: List[str]) -> None: ...
def send_getnext(
self: "SnmpV3ClientSocket", iter_getnext: GetNextIter
) -> None: ...
def send_getbulk(
self: "SnmpV3ClientSocket", iter_getbulk: GetBulkIter
) -> None: ...
def recv_getresponse(self: "SnmpV3ClientSocket") -> ValueType: ...
def recv_getresponse_many(
self: "SnmpV3ClientSocket",
) -> Dict[str, ValueType]: ...
def recv_getresponse_next(
self: "SnmpV3ClientSocket", iter_getnext: GetNextIter
) -> Tuple[str, ValueType]: ...
def recv_getresponse_bulk(
self: "SnmpV3ClientSocket", iter_getnext: GetBulkIter
) -> List[Tuple[str, ValueType]]: ...
22 changes: 21 additions & 1 deletion src/gufo/snmp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ._fast import (
SnmpV1ClientSocket,
SnmpV2cClientSocket,
SnmpV3ClientSocket,
)
from .getbulk import GetBulkIter
from .getnext import GetNextIter
Expand All @@ -34,7 +35,9 @@ class SnmpSession(object):
Args:
addr: SNMP agent address, either IPv4 or IPv6.
port: SNMP agent port.
community: SNMP community.
community: SNMP community (v1, v2c).
engine_id: SNMP Engine id (v3).
user_name: User name (v3).
version: Protocol version.
timeout: Request timeout in seconds.
tos: Set ToS/DSCP mark on egress packets.
Expand Down Expand Up @@ -68,6 +71,8 @@ def __init__(
addr: str,
port: int = 161,
community: str = "public",
engine_id: Optional[bytes] = None,
user_name: Optional[str] = None,
version: SnmpVersion = SnmpVersion.v2c,
timeout: float = 10.0,
tos: int = 0,
Expand Down Expand Up @@ -95,6 +100,21 @@ def __init__(
send_buffer,
recv_buffer,
)
elif version == SnmpVersion.v3:
if not user_name:
msg = "SNMPv3 requires user_name"
raise ValueError(msg)
if not engine_id:
msg = "SNMPv3 requires engine_id"
raise ValueError(msg)
self._sock = SnmpV3ClientSocket(
f"{addr}:{port}",
engine_id if engine_id else b"",
user_name,
tos,
send_buffer,
recv_buffer,
)
else:
msg = "Invalid SNMP Protocol"
raise ValueError(msg)
Expand Down
87 changes: 79 additions & 8 deletions src/gufo/snmp/snmpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,55 @@
# Python modules
import logging
import queue
import random
import string
import subprocess
import threading
from dataclasses import dataclass
from tempfile import NamedTemporaryFile, _TemporaryFileWrapper
from types import TracebackType
from typing import Optional, Type
from typing import List, Optional, Type

logger = logging.getLogger("gufo.snmp.snmpd")

# Net-snmp always adds prefix to engine id
_NETSNMP_ENGINE_ID_PREFIX = b"\x80\x00\x1f\x88\x04"
# Length of generated engine id
# Not including prefix.
_ENGINE_ID_LENGTH = 8


@dataclass
class User(object):
"""
SNMPv3 user.
Attributes:
name: user name
"""

name: str

@property
def rouser(self: "User") -> str:
"""
`rouser` part of config.
Returns:
rouser configuration directive.
"""
return f"rouser {self.name} noauth"

@property
def create_user(self: "User") -> str:
"""
createUser part of config.
Returns:
createUser configuration directive.
"""
return f"createUser {self.name}"


class Snmpd(object):
"""
Expand All @@ -34,12 +75,15 @@ class Snmpd(object):
community: SNMP v1/v2c community.
location: sysLocation value.
contact: sysContact value.
user: SNMP v3 user.
engine_id: Optional explicit engine id for SNMPv3.
Use generated value if not set.
users: Optional list of SNMPv3 users.
start_timeout: Maximum time to wait for snmpd to start.
log_packets: Log SNMP requests and responses.
Attributes:
version: Net-SNMP version.
engine_id: SNMPv3 engine id.
Note:
Using the ports below 1024 usually requires
Expand All @@ -66,7 +110,8 @@ def __init__(
community: str = "public",
location: str = "Test",
contact: str = "test <test@example.com>",
user: str = "rouser",
engine_id: Optional[str] = None,
users: Optional[List[User]] = None,
start_timeout: float = 5.0,
log_packets: bool = False,
) -> None:
Expand All @@ -76,12 +121,30 @@ def __init__(
self._community = community
self._location = location
self._contact = contact
self._user = user
self._users = users or [User(name="rouser")]
self._start_timeout = start_timeout
self._log_packets = log_packets
self.version: Optional[str] = None
self._cfg: Optional[_TemporaryFileWrapper[str]] = None
self._proc: Optional[subprocess.Popen[str]] = None
if engine_id:
self._cfg_engine_id = engine_id
else:
self._cfg_engine_id = self._get_engine_id()
self.engine_id = (
_NETSNMP_ENGINE_ID_PREFIX + self._cfg_engine_id.encode()
)

@staticmethod
def _get_engine_id() -> str:
"""
Generate random engine id.
Returns:
Random engine id for snmpd.conf
"""
chars = string.ascii_letters + string.digits
return "".join(random.choices(chars, k=_ENGINE_ID_LENGTH)) # noqa:S311

def get_config(self: "Snmpd") -> str:
"""
Expand All @@ -90,21 +153,30 @@ def get_config(self: "Snmpd") -> str:
Returns:
snmpd configuration.
"""
rousers = "\n".join(u.rouser for u in self._users)
create_users = "\n".join(u.create_user for u in self._users)

return f"""# Gufo SNMP Test Suite
master agentx
agentaddress udp:{self._address}:{self._port}
agentXsocket tcp:{self._address}:{self._port}
# SNMPv3 engine id
engineId {self._cfg_engine_id}
# Listen address
# SNMPv1/SNMPv2c R/O community
rocommunity {self._community} 127.0.0.1
# SNMPv3 R/O User
rouser {self._user} auth
{rousers}
{create_users}
# System information
syslocation {self._location}
syscontact {self._contact}
#
sysServices 72"""

# @todo: createUser
# http://www.net-snmp.org/docs/man/snmpd.conf.html

def _start(self: "Snmpd") -> None:
"""Run snmpd instance."""
logger.info("Starting snmpd instance")
Expand All @@ -127,9 +199,8 @@ def _start(self: "Snmpd") -> None:
"-V", # Verbose
]
if self._log_packets:
args += [
"-d", # Dump packets
]
args += ["-d"] # Dump packets
args += ["-Dsnmpd,dump,usm"]
logger.debug("Running: %s", " ".join(args))
self._proc = subprocess.Popen(
args,
Expand Down
1 change: 1 addition & 0 deletions src/gufo/snmp/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ class SnmpVersion(enum.IntEnum):

v1 = 0
v2c = 1
v3 = 3
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ fn gufo_ping(py: Python, m: &PyModule) -> PyResult<()> {
m.add("SnmpError", py.get_type::<error::PySnmpError>())?;
m.add("SnmpEncodeError", py.get_type::<error::PySnmpEncodeError>())?;
m.add("SnmpDecodeError", py.get_type::<error::PySnmpDecodeError>())?;
m.add("SnmpAuthError", py.get_type::<error::PySnmpAuthError>())?;
m.add("NoSuchInstance", py.get_type::<error::PyNoSuchInstance>())?;
m.add_class::<socket::SnmpV1ClientSocket>()?;
m.add_class::<socket::SnmpV2cClientSocket>()?;
m.add_class::<socket::SnmpV3ClientSocket>()?;
m.add_class::<socket::GetNextIter>()?;
m.add_class::<socket::GetBulkIter>()?;
Ok(())
Expand Down
3 changes: 3 additions & 0 deletions src/snmp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ use crate::ber::Tag;

const SNMP_V1: u8 = 0;
const SNMP_V2C: u8 = 1;
const SNMP_V3: u8 = 3;

const PDU_GET_REQUEST: Tag = 0;
const PDU_GETNEXT_REQUEST: Tag = 1;
const PDU_GET_RESPONSE: Tag = 2;
// const PDU_SET_REQUEST: Tag = 3;
// const PDU_TRAP: Tag = 4;
const PDU_GET_BULK_REQUEST: Tag = 5;
const PDU_REPORT: Tag = 8;

pub mod get;
pub mod getbulk;
pub mod getresponse;
pub mod msg;
pub mod pdu;
pub mod report;
pub mod value;
4 changes: 4 additions & 0 deletions src/snmp/msg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
// See LICENSE.md for details
// ------------------------------------------------------------------------

mod usm;
mod v1;
mod v2c;
mod v3;
pub use usm::UsmParameters;
pub use v1::SnmpV1Message;
pub use v2c::SnmpV2cMessage;
pub use v3::SnmpV3Message;
Loading

0 comments on commit 4933581

Please sign in to comment.