Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix unintended breakdown of MIB object sub-indexes into symbolic representations #389

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

lunkwill42
Copy link
Member

@lunkwill42 lunkwill42 commented Jan 21, 2025

Scope and purpose

This fixes a problem discovered in the Zino SNMP abstraction layer during the construction of an alternative SNMP abstraction layer: The _convert_varbind() function was intended to break down an object identity into a symbolic name and a numeric index (as represented by an Identifier object).

However, because of a misunderstanding of how PySNMP works, and a slight bug in the implementation of the Zino OID class, it turns out that the sub-indexes are also converted to symbolic representations if the MIB objects are defined that way - and the OID class appears to have allowed these non-integer elements to be represented within it.

This was not understood as unintended behavior when the BGP state monitor task was authored, causing workarounds for the strange behavior to be written in that plugin, rather than the bug to be reported :-)

It may actually be in our interest to have some MIB-assisted symbolic breakdown of object identities, but the PySNMP objects should not leak through our abstraction layer (which is slated to be replaced), so this is something we might want to look into at a later stage.

This pull request

  • Fixes a bug in the OID class.
  • Unrolls symbolic index objects back into numeric OIDs. It might not be an optimal solution, but the only other one I could think of was to translate the object back into an OID and then strip that prefix from the full object identity being translated. Both methods are kind of ugly.
  • Removes the now broken and obsolete workarounds from the BGP state monitor task.

Contributor Checklist

Every pull request should have this checklist filled out, no matter how small it is.
More information about contributing to Zino can be found in the
README.

  • Added a changelog fragment for towncrier
  • Added/amended tests for new/changed code
  • Added/changed documentation
  • Linted/formatted the code with black, ruff and isort, easiest by using pre-commit
  • The first line of the commit message continues the sentence "If applied, this commit will ...", starts with a capital letter, does not end with punctuation and is 50 characters or less long. See https://cbea.ms/git-commit/
  • If applicable: Created new issues if this PR does not fix the issue completely/there is further work to be done

@lunkwill42 lunkwill42 added the bug Something isn't working label Jan 21, 2025
@lunkwill42 lunkwill42 self-assigned this Jan 21, 2025
Copy link

github-actions bot commented Jan 21, 2025

🦙 MegaLinter status: ✅ SUCCESS

Descriptor Linter Files Fixed Errors Elapsed time
✅ PYTHON black 4 0 0.73s
✅ PYTHON isort 4 0 0.25s
✅ PYTHON ruff 4 0 0.01s

See detailed report in MegaLinter reports
Set VALIDATE_ALL_CODEBASE: true in mega-linter.yml to validate all sources, not only the diff

MegaLinter is graciously provided by OX Security

Copy link

github-actions bot commented Jan 21, 2025

Test results

    3 files      3 suites   1m 29s ⏱️
  587 tests   587 ✅ 0 💤 0 ❌
1 737 runs  1 735 ✅ 2 💤 0 ❌

Results for commit 83d6c4e.

♻️ This comment has been updated with latest results.

@lunkwill42 lunkwill42 marked this pull request as ready for review January 21, 2025 13:47
@lunkwill42 lunkwill42 requested a review from stveit January 21, 2025 13:48
Copy link

codecov bot commented Jan 21, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.66%. Comparing base (1e9df5c) to head (83d6c4e).
Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #389   +/-   ##
=======================================
  Coverage   98.66%   98.66%           
=======================================
  Files          77       77           
  Lines        9711     9715    +4     
=======================================
+ Hits         9581     9585    +4     
  Misses        130      130           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

The OID class is designed to represent a tuple of ints.  It was never
designed to contain non-int elements (although it can translate non-int
values to int elements when instances are constructed).

The Zino code has bugs that actually would feed the OID class with
non-int elements, and because the OID class accepted it, strange
workarounds have been written by those who did not understand that
this was not the intended behavior.

This change enforces all elements to be int on instantiation -
presumably at the cost of instantiation becoming slower.
An `Identifier` was intended to represent an object's MIB name,
symbolic object name and numeric OID index.  However, due to a
misunderstanding of how PySNMP works, this was not always the case:

`getMibSymbol` will actually break down the remaining OID index to
one or more symbolic indices according to the MIB specification, so
the third element of the `getMibSymbol()` call may actually be a tuple
of various objects that represents individual indices.

While this could be helpful in the codebase, it was not the intention
of `_convert_varbind` and `Identifier`.  To keep Zino's interface
as intended here, this change tries to unroll all the symbolic indices
back to a single OID object - likely with some performance penalty.

We might want to look into changing the codebase to work with symbolic
indexes at a later time.
bgpstatemonitortask.py contained some strange workarounds for behaviors
that were actually bugs in the `_convert_varbind` implementation.
These bugs have been fixed, causing the workarounds to fail: This fixes
the failures by removing the workarounds.
@lunkwill42 lunkwill42 force-pushed the bugfix/symbolic-oid-interpretation-issues branch from 14ad020 to 29b2441 Compare January 21, 2025 13:55
src/zino/snmp.py Outdated
Comment on lines 509 to 522
def _index_to_tuple(index: ObjectIdentity) -> Tuple[int, ...]:
"""Unrolls a PySNMP index object to a tuple of integers"""
try:
return tuple(index)
except TypeError:
try:
return index.asTuple()
except AttributeError:
try:
return index.getOid()
except AttributeError:
return (int(index),)


Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon testing the new SNMP framework I've found that this still isn't up to snuff, as some bits keep getting cut off. Notably, an object like .1.3.6.1.4.1.9.9.187.1.2.5.1.4.1.4.10.0.0.1 would normally be broken down to CISCO-BGP4-MIB::cbgpPeer2AdminStatus.ipv4."10.0.0.1", but what we want from our function is something more like CISCO-BGP4-MIB::cbgpPeer2AdminStatus.1.4.10.0.0.1. I.e. the index returned should be 1.4.10.0.0.1 (1 is the type of address represented, 4 is the number of subsequent octets that represent the address) - instead this only return 4.10.0.0.1. Only the bgpstatemonitortask cares right now, but this PR makes it expect the latter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a new commit where the strategy becomes more or less the same as in the netsnmp-cffi replacement branch: Reconstruct a prefix from the mib and object name, and then strip that from the incoming OID argument to get the row_index suffix value.

The table index for an address like `10.0.0.42` will be
`.1.4.10.0.0.42`, where `1 is the address type and `4` is the address
length - while the remainder is the address itself.  We don't really
need the `1` and the `4` to detect the address type, as `ip_address`
will parse things correctly if given just a series of bytes.

It turns out that the index unrolling first implemented in
`_convert_varbind()` will cause some elements of the `row_index` OID to
be left out, causing mayhem for client code that relies on these values.
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant