From f6c2a90aef8c594c4ca693cb308bf978a3d530e4 Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Thu, 23 Jul 2026 20:29:32 +0200 Subject: [PATCH 1/5] chore: migrate to pyproject.toml, drop orphaned test.py Move static project metadata to pyproject.toml (PEP 621). setup.py is slimmed to just the C extension, whose net-snmp libraries are still discovered dynamically via net-snmp-config and can't be declared statically. Also remove netsnmp/tests/test.py: it is the legacy monolithic bindings test, not run by CI (the tests/system/ suite superseded it) and not matched by the unittest discover glob. The stale test_suite= reference in setup.py pointing at it is dropped along with the old dead distutils/ string imports and broken --basedir parsing. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014KG1c9XtKawkvig5PnZjC8 --- netsnmp/tests/test.py | 321 ------------------------------------------ pyproject.toml | 31 ++++ setup.py | 50 +++---- 3 files changed, 50 insertions(+), 352 deletions(-) delete mode 100644 netsnmp/tests/test.py create mode 100644 pyproject.toml diff --git a/netsnmp/tests/test.py b/netsnmp/tests/test.py deleted file mode 100644 index 4787e9f..0000000 --- a/netsnmp/tests/test.py +++ /dev/null @@ -1,321 +0,0 @@ -""" Runs all unit tests for the netsnmp package. """ -# Copyright (c) 2006 Andy Gross. See LICENSE.txt for details. - -import sys -import unittest -import netsnmp -import time - -class BasicTests(unittest.TestCase): - def testFuncs(self): - print("") - var = netsnmp.Varbind('sysDescr.0') - var = netsnmp.Varbind('sysDescr','0') - var = netsnmp.Varbind( - '.iso.org.dod.internet.mgmt.mib-2.system.sysDescr','0') - var = netsnmp.Varbind( - '.iso.org.dod.internet.mgmt.mib-2.system.sysDescr.0') - var = netsnmp.Varbind('.1.3.6.1.2.1.1.1.0') - - var = netsnmp.Varbind('.1.3.6.1.2.1.1.1','0') - - print("---v1 GET tests -------------------------------------\n") - res = netsnmp.snmpget(var, - Version = 1, - DestHost='localhost', - Community='public') - - print("v1 snmpget result: ", res, "\n") - - print("v1 get var: ", var.tag, var.iid, "=", var.val, '(',var.type,')') - - print("---v1 GETNEXT tests-------------------------------------\n") - res = netsnmp.snmpgetnext(var, - Version = 1, - DestHost='localhost', - Community='public') - - print("v1 snmpgetnext result: ", res, "\n") - - print("v1 getnext var: ", var.tag, var.iid, "=", var.val, '(',var.type,')') - - print("---v1 SET tests-------------------------------------\n") - var = netsnmp.Varbind('sysLocation','0', 'my new location') - res = netsnmp.snmpset(var, - Version = 1, - DestHost='localhost', - Community='public') - - print("v1 snmpset result: ", res, "\n") - - print("v1 set var: ", var.tag, var.iid, "=", var.val, '(',var.type,')') - - print("---v1 walk tests-------------------------------------\n") - vars = netsnmp.VarList(netsnmp.Varbind('system')) - - print("v1 varlist walk in: ") - for var in vars: - print(" ",var.tag, var.iid, "=", var.val, '(',var.type,')') - - res = netsnmp.snmpwalk(vars, - Version = 1, - DestHost='localhost', - Community='public') - print("v1 snmpwalk result: ", res, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - - - print("---v1 walk 2-------------------------------------\n") - - print("v1 varbind walk in: ") - var = netsnmp.Varbind('system') - res = netsnmp.snmpwalk(var, - Version = 1, - DestHost='localhost', - Community='public') - print("v1 snmpwalk result (should be = orig): ", res, "\n") - - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - - print("---v1 multi-varbind test-------------------------------------\n") - sess = netsnmp.Session(Version=1, - DestHost='localhost', - Community='public') - - vars = netsnmp.VarList(netsnmp.Varbind('sysUpTime', 0), - netsnmp.Varbind('sysContact', 0), - netsnmp.Varbind('sysLocation', 0)) - vals = sess.get(vars) - print("v1 sess.get result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - - vals = sess.getnext(vars) - print("v1 sess.getnext result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - - vars = netsnmp.VarList(netsnmp.Varbind('sysUpTime'), - netsnmp.Varbind('sysORLastChange'), - netsnmp.Varbind('sysORID'), - netsnmp.Varbind('sysORDescr'), - netsnmp.Varbind('sysORUpTime')) - - vals = sess.getbulk(2, 8, vars) - print("v1 sess.getbulk result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - - print("---v1 set2-------------------------------------\n") - - vars = netsnmp.VarList( - netsnmp.Varbind('sysLocation', '0', 'my newer location')) - res = sess.set(vars) - print("v1 sess.set result: ", res, "\n") - - print("---v1 walk3-------------------------------------\n") - vars = netsnmp.VarList(netsnmp.Varbind('system')) - - vals = sess.walk(vars) - print("v1 sess.walk result: ", vals, "\n") - - for var in vars: - print(" ",var.tag, var.iid, "=", var.val, '(',var.type,')') - - print("---v2c get-------------------------------------\n") - - sess = netsnmp.Session(Version=2, - DestHost='localhost', - Community='public') - - sess.UseEnums = 1 - sess.UseLongNames = 1 - - vars = netsnmp.VarList(netsnmp.Varbind('sysUpTime', 0), - netsnmp.Varbind('sysContact', 0), - netsnmp.Varbind('sysLocation', 0)) - vals = sess.get(vars) - print("v2 sess.get result: ", vals, "\n") - - print("---v2c getnext-------------------------------------\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - vals = sess.getnext(vars) - print("v2 sess.getnext result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - print("---v2c getbulk-------------------------------------\n") - - vars = netsnmp.VarList(netsnmp.Varbind('sysUpTime'), - netsnmp.Varbind('sysORLastChange'), - netsnmp.Varbind('sysORID'), - netsnmp.Varbind('sysORDescr'), - netsnmp.Varbind('sysORUpTime')) - - vals = sess.getbulk(2, 8, vars) - print("v2 sess.getbulk result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - print("---v2c set-------------------------------------\n") - - vars = netsnmp.VarList( - netsnmp.Varbind('sysLocation','0','my even newer location')) - - res = sess.set(vars) - print("v2 sess.set result: ", res, "\n") - - print("---v2c walk-------------------------------------\n") - vars = netsnmp.VarList(netsnmp.Varbind('system')) - - vals = sess.walk(vars) - print("v2 sess.walk result: ", vals, "\n") - - for var in vars: - print(" ",var.tag, var.iid, "=", var.val, '(',var.type,')') - - print("---v3 setup-------------------------------------\n") - sess = netsnmp.Session(Version=3, - DestHost='localhost', - SecLevel='authPriv', - SecName='initial', - PrivPass='priv_pass', - AuthPass='auth_pass') - - sess.UseSprintValue = 1 - - vars = netsnmp.VarList(netsnmp.Varbind('sysUpTime', 0), - netsnmp.Varbind('sysContact', 0), - netsnmp.Varbind('sysLocation', 0)) - print("---v3 get-------------------------------------\n") - vals = sess.get(vars) - print("v3 sess.get result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - print("---v3 getnext-------------------------------------\n") - - vals = sess.getnext(vars) - print("v3 sess.getnext result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - vars = netsnmp.VarList(netsnmp.Varbind('sysUpTime'), - netsnmp.Varbind('sysORLastChange'), - netsnmp.Varbind('sysORID'), - netsnmp.Varbind('sysORDescr'), - netsnmp.Varbind('sysORUpTime')) - - vals = sess.getbulk(2, 8, vars) - print("v3 sess.getbulk result: ", vals, "\n") - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - print("---v3 set-------------------------------------\n") - - vars = netsnmp.VarList( - netsnmp.Varbind('sysLocation','0', 'my final destination')) - res = sess.set(vars) - print("v3 sess.set result: ", res, "\n") - - print("---v3 walk-------------------------------------\n") - vars = netsnmp.VarList(netsnmp.Varbind('system')) - - vals = sess.walk(vars) - print("v3 sess.walk result: ", vals, "\n") - - for var in vars: - print(" ",var.tag, var.iid, "=", var.val, '(',var.type,')') - - -class SetTests(unittest.TestCase): - def testFuncs(self): - print("\n-------------- SET Test Start ----------------------------\n") - - var = netsnmp.Varbind('sysUpTime','0') - res = netsnmp.snmpget(var, Version = 1, DestHost='localhost', - Community='public') - print("uptime = ", res[0]) - - - var = netsnmp.Varbind('versionRestartAgent','0', 1) - res = netsnmp.snmpset(var, Version = 1, DestHost='localhost', - Community='public') - - var = netsnmp.Varbind('sysUpTime','0') - res = netsnmp.snmpget(var, Version = 1, DestHost='localhost', - Community='public') - print("uptime = ", res[0]) - - var = netsnmp.Varbind('nsCacheEntry') - res = netsnmp.snmpgetnext(var, Version = 1, DestHost='localhost', - Community='public') - print("var = ", var.tag, var.iid, "=", var.val, '(',var.type,')') - - var.val = 65 - res = netsnmp.snmpset(var, Version = 1, DestHost='localhost', - Community='public') - res = netsnmp.snmpget(var, Version = 1, DestHost='localhost', - Community='public') - print("var = ", var.tag, var.iid, "=", var.val, '(',var.type,')') - - sess = netsnmp.Session(Version = 1, DestHost='localhost', - Community='public') - - vars = netsnmp.VarList(netsnmp.Varbind('.1.3.6.1.6.3.12.1.2.1.2.116.101.115.116','','.1.3.6.1.6.1.1'), - netsnmp.Varbind('.1.3.6.1.6.3.12.1.2.1.3.116.101.115.116','','1234'), - netsnmp.Varbind('.1.3.6.1.6.3.12.1.2.1.9.116.101.115.116','', 4)) - res = sess.set(vars) - - print("res = ", res) - - vars = netsnmp.VarList(netsnmp.Varbind('snmpTargetAddrTDomain'), - netsnmp.Varbind('snmpTargetAddrTAddress'), - netsnmp.Varbind('snmpTargetAddrRowStatus')) - - res = sess.getnext(vars) - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - vars = netsnmp.VarList(netsnmp.Varbind('.1.3.6.1.6.3.12.1.2.1.9.116.101.115.116','', 6)) - - res = sess.set(vars) - - print("res = ", res) - - vars = netsnmp.VarList(netsnmp.Varbind('snmpTargetAddrTDomain'), - netsnmp.Varbind('snmpTargetAddrTAddress'), - netsnmp.Varbind('snmpTargetAddrRowStatus')) - - res = sess.getnext(vars) - - for var in vars: - print(var.tag, var.iid, "=", var.val, '(',var.type,')') - print("\n") - - print("\n-------------- SET Test End ----------------------------\n") - - -if __name__=='__main__': - unittest.main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..53f4806 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "python3-netsnmp" +version = "1.1a2" +description = "The Net-SNMP Python Interface" +readme = { file = "README", content-type = "text/plain" } +requires-python = ">=3.6" +license = { text = "BSD" } +authors = [ + { name = "G. S. Marzot", email = "giovanni.marzot@sparta.com" }, +] +maintainers = [ + { name = "Christian Svensson", email = "blue@cmd.nu" }, +] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Topic :: System :: Networking :: Monitoring", +] + +[project.urls] +Homepage = "http://www.net-snmp.org" +Source = "https://github.com/bluecmd/python3-netsnmp" + +# The C extension (net-snmp client bindings) is built from setup.py, which +# discovers the net-snmp libraries dynamically via net-snmp-config. +[tool.setuptools.packages.find] +include = ["netsnmp*"] diff --git a/setup.py b/setup.py index e3a65f6..46b5ddd 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,15 @@ -from distutils.core import setup, Extension -from setuptools import setup, Extension, find_packages +"""Build the netsnmp C extension. + +Static project metadata lives in pyproject.toml. This file only remains for +the parts that can't be expressed declaratively: the net-snmp client library +is discovered at build time via net-snmp-config. +""" import os import re -import string import sys -args = sys.argv[:] -for arg in args: - if '--basedir' in arg: - basedir = string.split(arg,'=')[1] - sys.argv.remove(arg) +from setuptools import Extension, setup + def netsnmp_config(flag): cmd = 'net-snmp-config ' + flag @@ -20,6 +20,7 @@ def netsnmp_config(flag): cmd = 'sh -c "net-snmp-config %s"' % flag return os.popen(cmd).read() + netsnmp_libs = netsnmp_config('--libs') libdirs = re.findall(r" -L(\S+)", netsnmp_libs) incdirs = [] @@ -29,26 +30,13 @@ def netsnmp_config(flag): libs.append('ws2_32') setup( - name="python3-netsnmp", version="1.1a2", - description = 'The Net-SNMP Python Interface', - long_description = ''' -Python3 port of the official Net-SNMP Python bindings. - -Maintainer: Christian Svensson - -Source: https://github.com/bluecmd/python3-netsnmp -''', - author = 'G. S. Marzot', - author_email = 'giovanni.marzot@sparta.com', - url = 'http://www.net-snmp.org', - license="BSD", - packages=find_packages(), - test_suite = "netsnmp.tests.test", - - ext_modules = [ - Extension("netsnmp.client_intf", ["netsnmp/client_intf.c"], - library_dirs=libdirs, - include_dirs=incdirs, - libraries=libs ) - ] - ) + ext_modules=[ + Extension( + "netsnmp.client_intf", + ["netsnmp/client_intf.c"], + library_dirs=libdirs, + include_dirs=incdirs, + libraries=libs, + ) + ], +) From ff1a070381d43fc0184904405f30093a32edb423 Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Thu, 23 Jul 2026 22:24:10 +0200 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20add=20Markus=20H=C3=A4ll=20as=20co?= =?UTF-8?q?-maintainer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markus Häll has contributed to the project (including the memory ownership fixes in netsnmp_walk) and is added alongside Christian Svensson in the project maintainers metadata. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014KG1c9XtKawkvig5PnZjC8 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 53f4806..6b69f2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ authors = [ ] maintainers = [ { name = "Christian Svensson", email = "blue@cmd.nu" }, + { name = "Markus Häll", email = "soundgoof@gmail.com" }, ] classifiers = [ "License :: OSI Approved :: BSD License", From 36041547b3ae2eee57a498eaa6530e95e1acb68c Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Thu, 23 Jul 2026 23:09:35 +0200 Subject: [PATCH 3/5] test: restore SNMPv3 coverage, require Python >= 3.7 Addresses PR review feedback: - requires-python is raised from >=3.6 to >=3.7. The build-system pin of setuptools>=61 cannot be satisfied on 3.6, so the PEP 517 build failed while creating the isolated build env. - Removing the legacy netsnmp/tests/test.py dropped the only SNMPv3 coverage, as the system suite only exercised v1/v2c. Add an SNMPv3 authPriv (SHA/AES) user to the test snmpd config and a test_v3.py covering get, getnext, getbulk, set, walk and the session API. Unlike the legacy test, which only printed results, these assert. A negative case with a bad AuthPass proves the agent really enforces authPriv, so the positive tests cannot silently pass unauthenticated. Also ignore __pycache__/*.pyc, which were previously untracked-but-noisy. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014KG1c9XtKawkvig5PnZjC8 --- .gitignore | 2 ++ netsnmp/tests/system/common.py | 15 +++++++++ netsnmp/tests/system/snmpd.conf | 6 ++++ netsnmp/tests/system/test_v3.py | 60 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 netsnmp/tests/system/test_v3.py diff --git a/.gitignore b/.gitignore index 0f22a4e..2461d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ build/ dist/ +__pycache__/ +*.py[co] .*.swp .*.swo *.egg-info/ diff --git a/netsnmp/tests/system/common.py b/netsnmp/tests/system/common.py index 2c68326..934ba72 100644 --- a/netsnmp/tests/system/common.py +++ b/netsnmp/tests/system/common.py @@ -9,6 +9,21 @@ WRITE_ARGS = READ_ARGS.copy() WRITE_ARGS['Community'] = 'private' +# SNMPv3 authPriv credentials. Keep in sync with the createUser/rwuser +# directives in netsnmp/tests/system/snmpd.conf. +V3_ARGS = { + 'Version': 3, + 'DestHost': HOST, + 'SecLevel': 'authPriv', + 'SecName': 'testuser', + 'AuthProto': 'SHA', + 'AuthPass': 'auth_pass', + 'PrivProto': 'AES', + 'PrivPass': 'priv_pass', + 'Timeout': 1000000, + 'Retries': 0, +} + SYS_DESCR = '.1.3.6.1.2.1.1.1' SYS_UPTIME = '.1.3.6.1.2.1.1.3' SYS_LOCATION = '.1.3.6.1.2.1.1.6' diff --git a/netsnmp/tests/system/snmpd.conf b/netsnmp/tests/system/snmpd.conf index e986783..d01a6dd 100644 --- a/netsnmp/tests/system/snmpd.conf +++ b/netsnmp/tests/system/snmpd.conf @@ -1,3 +1,9 @@ agentAddress udp:127.0.0.1:1161 rocommunity public 127.0.0.1 rwcommunity private 127.0.0.1 + +# SNMPv3 USM user used by test_v3.py. Passphrases must be at least 8 +# characters for net-snmp to accept them. Keep in sync with V3_ARGS in +# netsnmp/tests/system/common.py. +createUser testuser SHA "auth_pass" AES "priv_pass" +rwuser testuser authPriv diff --git a/netsnmp/tests/system/test_v3.py b/netsnmp/tests/system/test_v3.py new file mode 100644 index 0000000..8ba1b52 --- /dev/null +++ b/netsnmp/tests/system/test_v3.py @@ -0,0 +1,60 @@ +import unittest + +import netsnmp + +from netsnmp.tests.system.common import ( + SYS_DESCR, + SYS_LOCATION, + SYSTEM, + V3_ARGS, + assert_value, +) + + +class SnmpV3Tests(unittest.TestCase): + """SNMPv3 authPriv coverage (USM authentication and privacy).""" + + def test_get(self): + values = netsnmp.snmpget(netsnmp.Varbind(SYS_DESCR, '0'), **V3_ARGS) + assert_value(self, values) + + def test_getnext(self): + values = netsnmp.snmpgetnext(netsnmp.Varbind(SYSTEM), **V3_ARGS) + assert_value(self, values) + + def test_getbulk(self): + session = netsnmp.Session(**V3_ARGS) + varlist = netsnmp.VarList(netsnmp.Varbind(SYSTEM)) + assert_value(self, session.getbulk(0, 4, varlist)) + self.assertGreater(len(varlist), 1) + + def test_set(self): + value = b'snmp-v3' + result = netsnmp.snmpset( + netsnmp.Varbind(SYS_LOCATION, '0', value, 'OCTETSTR'), **V3_ARGS) + self.assertEqual(result, 1) + self.assertEqual(netsnmp.snmpget( + netsnmp.Varbind(SYS_LOCATION, '0'), **V3_ARGS)[0], value) + + def test_walk(self): + varlist = netsnmp.VarList(netsnmp.Varbind(SYSTEM)) + assert_value(self, netsnmp.snmpwalk(varlist, **V3_ARGS)) + self.assertGreater(len(varlist), 1) + + def test_session_get(self): + session = netsnmp.Session(**V3_ARGS) + varlist = netsnmp.VarList(netsnmp.Varbind(SYS_DESCR, '0')) + assert_value(self, session.get(varlist)) + + def test_wrong_auth_password_is_rejected(self): + """Proves the agent really enforces authPriv, so the tests above + are not silently passing over an unauthenticated session.""" + args = dict(V3_ARGS, AuthPass='wrong_auth_pass') + session = netsnmp.Session(**args) + varlist = netsnmp.VarList(netsnmp.Varbind(SYS_DESCR, '0')) + values = session.get(varlist) + self.assertFalse(values and values[0]) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyproject.toml b/pyproject.toml index 6b69f2f..b66d069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "python3-netsnmp" version = "1.1a2" description = "The Net-SNMP Python Interface" readme = { file = "README", content-type = "text/plain" } -requires-python = ">=3.6" +requires-python = ">=3.7" license = { text = "BSD" } authors = [ { name = "G. S. Marzot", email = "giovanni.marzot@sparta.com" }, From 453beb02b252cea0d19f5f4372ece343439e9cec Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Thu, 23 Jul 2026 23:21:03 +0200 Subject: [PATCH 4/5] test: fix SNMPv3 negative test against USM key cache The wrong-password test passed authentication because net-snmp caches localized USM keys per (engineID, username) for the process lifetime. An earlier test had already authenticated as testuser with the correct passphrase, so the cached key was reused and the bad passphrase never reached the agent -- the test was asserting on the cache, not the agent. Use a dedicated 'baduser' that no passing test ever authenticates as, so its cache entry is only ever populated from the wrong passphrase. Also add an unknown-user case, which is rejected agent-side and so cannot be affected by client caching at all. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014KG1c9XtKawkvig5PnZjC8 --- netsnmp/tests/system/snmpd.conf | 8 ++++++++ netsnmp/tests/system/test_v3.py | 21 +++++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/netsnmp/tests/system/snmpd.conf b/netsnmp/tests/system/snmpd.conf index d01a6dd..c93d84c 100644 --- a/netsnmp/tests/system/snmpd.conf +++ b/netsnmp/tests/system/snmpd.conf @@ -7,3 +7,11 @@ rwcommunity private 127.0.0.1 # netsnmp/tests/system/common.py. createUser testuser SHA "auth_pass" AES "priv_pass" rwuser testuser authPriv + +# Only ever contacted with a deliberately wrong passphrase, by +# test_wrong_auth_password_is_rejected. It must not be used by any passing +# test: net-snmp caches localized USM keys per (engineID, username) for the +# life of the process, so a successful auth would prime that cache and mask +# the rejection we are asserting. +createUser baduser SHA "correct_auth_pass" AES "correct_priv_pass" +rouser baduser authPriv diff --git a/netsnmp/tests/system/test_v3.py b/netsnmp/tests/system/test_v3.py index 8ba1b52..7d15319 100644 --- a/netsnmp/tests/system/test_v3.py +++ b/netsnmp/tests/system/test_v3.py @@ -46,11 +46,24 @@ def test_session_get(self): varlist = netsnmp.VarList(netsnmp.Varbind(SYS_DESCR, '0')) assert_value(self, session.get(varlist)) + def test_unknown_user_is_rejected(self): + session = netsnmp.Session(**dict(V3_ARGS, SecName='nosuchuser')) + varlist = netsnmp.VarList(netsnmp.Varbind(SYS_DESCR, '0')) + values = session.get(varlist) + self.assertFalse(values and values[0]) + def test_wrong_auth_password_is_rejected(self): - """Proves the agent really enforces authPriv, so the tests above - are not silently passing over an unauthenticated session.""" - args = dict(V3_ARGS, AuthPass='wrong_auth_pass') - session = netsnmp.Session(**args) + """Proves the agent really enforces authPriv, so the tests above are + not silently passing over an unauthenticated session. + + Uses a dedicated user rather than the one above: net-snmp caches + localized USM keys per (engineID, username) process-wide, so reusing + a name that already authenticated successfully would reuse the good + key and the wrong passphrase would never reach the agent. + """ + session = netsnmp.Session( + **dict(V3_ARGS, SecName='baduser', AuthPass='wrong_auth_pass', + PrivPass='wrong_priv_pass')) varlist = netsnmp.VarList(netsnmp.Varbind(SYS_DESCR, '0')) values = session.get(varlist) self.assertFalse(values and values[0]) From 96353769c6f2fd96b0bc7fb51d9879ab2d7096ef Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Thu, 23 Jul 2026 23:23:52 +0200 Subject: [PATCH 5/5] fix: free security protocol OIDs in netsnmp_create_session_v3 snmp_duplicate_objid() allocates the securityAuthProto and securityPrivProto OIDs, but they were never released. snmp_sess_open() duplicates them into the session it returns, so the local copies are the caller's to free -- they leaked on every SNMPv3 session creation (1440 bytes over 18 blocks across the new v3 tests). Caught by Valgrind once the new test_v3.py gave the v3 code path its first coverage. Freeing at the 'end' label also covers the early goto paths, where an unsupported privacy protocol returns after the authentication protocol OID was already duplicated. session is {0} initialised, so free() on the unset paths is a no-op. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014KG1c9XtKawkvig5PnZjC8 --- netsnmp/client_intf.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netsnmp/client_intf.c b/netsnmp/client_intf.c index 14f943d..7d58302 100644 --- a/netsnmp/client_intf.c +++ b/netsnmp/client_intf.c @@ -1346,6 +1346,12 @@ netsnmp_create_session_v3(PyObject *self, PyObject *args) end: free (session.securityEngineID); free (session.contextEngineID); + /* snmp_sess_open() duplicates the security protocol OIDs into the session + it returns, so the copies made above are ours to release. Freeing them + here also covers the early "goto end" paths, where the privacy protocol + is rejected after the authentication protocol was already duplicated. */ + free (session.securityAuthProto); + free (session.securityPrivProto); if (PyErr_Occurred()) { return NULL;