From 6ebb6de10743f0db13e0dbcff488013d0adcaad6 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Mon, 10 Nov 2025 10:21:01 -0500 Subject: [PATCH] Allow specifiying SNMP privacy protocol Modern SNMP devices may require AES. Unfortunately, older ones may refuse AES. For compatibility, continue to default to DES, but allow AES to be indicated in attributes. --- .../confluent/config/attributes.py | 4 +++ confluent_server/confluent/networking/lldp.py | 14 ++++++++--- .../confluent/networking/macmap.py | 25 +++++++++---------- .../confluent/networking/netutil.py | 6 +++-- confluent_server/confluent/snmputil.py | 12 +++++++-- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index 26f77732..dc5b8d40 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -605,6 +605,10 @@ node = { 'description': ('SNMPv1 community string, it is highly recommended to' 'step up to SNMPv3'), }, + 'snmp.privacyprotocol': { + 'description': 'The privacy protocol to use for SNMPv3', + 'valid_values': ('aes', 'des'), + }, # 'secret.snmplocalizedkey': { # 'description': ("SNMPv3 key localized to this node's SNMP Engine id" # 'This can be used in lieu of snmppassphrase to avoid' diff --git a/confluent_server/confluent/networking/lldp.py b/confluent_server/confluent/networking/lldp.py index 5f17ab80..e2eba7e3 100644 --- a/confluent_server/confluent/networking/lldp.py +++ b/confluent_server/confluent/networking/lldp.py @@ -255,7 +255,13 @@ def _extract_neighbor_data_b(args): args are carried as a tuple, because of eventlet convenience """ - switch, password, user, cfm, force = args[:5] + # Safely unpack args with defaults to avoid IndexError + switch = args[0] if len(args) > 0 else None + password = args[1] if len(args) > 1 else None + user = args[2] if len(args) > 2 else None + cfm = args[3] if len(args) > 3 else None + privproto = args[4] if len(args) > 4 else None + force = args[5] if len(args) > 5 else False vintage = _neighdata.get(switch, {}).get('!!vintage', 0) now = util.monotonic_time() if vintage > (now - 60) and not force: @@ -265,7 +271,7 @@ def _extract_neighbor_data_b(args): return _extract_neighbor_data_https(switch, user, password, cfm, lldpdata) except Exception as e: pass - conn = snmp.Session(switch, password, user) + conn = snmp.Session(switch, password, user, privacy_protocol=privproto) sid = None for sysid in conn.walk('1.3.6.1.2.1.1.2'): sid = str(sysid[1][6:]) @@ -364,8 +370,8 @@ def _extract_neighbor_data(args): return _extract_neighbor_data_b(args) except Exception as e: yieldexc = False - if len(args) >= 6: - yieldexc = args[5] + if len(args) >= 7: + yieldexc = args[6] if yieldexc: return e else: diff --git a/confluent_server/confluent/networking/macmap.py b/confluent_server/confluent/networking/macmap.py index 5545e09a..a96a48ee 100644 --- a/confluent_server/confluent/networking/macmap.py +++ b/confluent_server/confluent/networking/macmap.py @@ -213,14 +213,14 @@ def _fast_backend_fixup(macs, switch): else: _nodesbymac[mac] = (nodename, nummacs) -def _offload_map_switch(switch, password, user): +def _offload_map_switch(switch, password, user, privprotocol=None): if _offloader is None: _start_offloader() evtid = random.randint(0, 4294967295) while evtid in _offloadevts: evtid = random.randint(0, 4294967295) _offloadevts[evtid] = eventlet.Event() - _offloader.stdin.write(msgpack.packb((evtid, switch, password, user), + _offloader.stdin.write(msgpack.packb((evtid, switch, password, user, privprotocol), use_bin_type=True)) _offloader.stdin.flush() result = _offloadevts[evtid].wait() @@ -280,12 +280,11 @@ def _map_switch_backend(args): # fallback if ifName is empty # global _macmap - if len(args) == 4: - switch, password, user, _ = args # 4th arg is for affluent only - if not user: - user = None - else: - switch, password = args + switch = args[0] if len(args) > 0 else None + password = args[1] if len(args) > 1 else None + user = args[2] if len(args) > 2 else None + privprotocol = args[4] if len(args) > 4 else None + if not user: # make '' be treated as None user = None if switch not in noaffluent: try: @@ -298,7 +297,7 @@ def _map_switch_backend(args): except Exception as e: pass mactobridge, ifnamemap, bridgetoifmap = _offload_map_switch( - switch, password, user) + switch, password, user, privprotocol) maccounts = {} bridgetoifvalid = False for mac in mactobridge: @@ -367,9 +366,9 @@ def _map_switch_backend(args): _nodesbymac[mac] = (nodename, maccounts[ifname]) _macsbyswitch[switch] = newmacs -def _snmp_map_switch_relay(rqid, switch, password, user): +def _snmp_map_switch_relay(rqid, switch, password, user, privprotocol=None): try: - res = _snmp_map_switch(switch, password, user) + res = _snmp_map_switch(switch, password, user, privprotocol) payload = msgpack.packb((rqid,) + res, use_bin_type=True) try: sys.stdout.buffer.write(payload) @@ -391,10 +390,10 @@ def _snmp_map_switch_relay(rqid, switch, password, user): finally: sys.stdout.flush() -def _snmp_map_switch(switch, password, user): +def _snmp_map_switch(switch, password, user, privprotocol=None): haveqbridge = False mactobridge = {} - conn = snmp.Session(switch, password, user) + conn = snmp.Session(switch, password, user, privacy_protocol=privprotocol) ifnamemap = get_portnamemap(conn) for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'): haveqbridge = True diff --git a/confluent_server/confluent/networking/netutil.py b/confluent_server/confluent/networking/netutil.py index 48b2f028..65cd8236 100644 --- a/confluent_server/confluent/networking/netutil.py +++ b/confluent_server/confluent/networking/netutil.py @@ -21,7 +21,7 @@ import confluent.collective.manager as collective def get_switchcreds(configmanager, switches): switchcfg = configmanager.get_node_attributes( switches, ('secret.hardwaremanagementuser', 'secret.snmpcommunity', - 'secret.hardwaremanagementpassword', + 'secret.hardwaremanagementpassword', 'snmp.privacyprotocol', 'collective.managercandidates'), decrypt=True) switchauth = [] for switch in switches: @@ -47,7 +47,9 @@ def get_switchcreds(configmanager, switches): 'secret.hardwaremanagementuser', {}).get('value', None) if not user: user = None - switchauth.append((switch, password, user, configmanager)) + privacy_protocol = switchparms.get( + 'snmp.privacyprotocol', {}).get('value', None) + switchauth.append((switch, password, user, configmanager, privacy_protocol)) return switchauth diff --git a/confluent_server/confluent/snmputil.py b/confluent_server/confluent/snmputil.py index ce6e3759..6f03a31b 100644 --- a/confluent_server/confluent/snmputil.py +++ b/confluent_server/confluent/snmputil.py @@ -78,7 +78,7 @@ def _get_transport(name): class Session(object): - def __init__(self, server, secret, username=None, context=None): + def __init__(self, server, secret, username=None, context=None, privacy_protocol=None): """Create a new session to interrogate a switch If username is not given, it is assumed that @@ -97,9 +97,17 @@ class Session(object): # SNMP v2c self.authdata = snmp.CommunityData(secret, mpModel=1) else: + if privacy_protocol == 'aes': + privproto = snmp.usmAesCfb128Protocol + elif privacy_protocol in ('des', None): + privproto = snmp.usmDESPrivProtocol + else: + raise exc.ConfluentException('Unsupported SNMPv3 privacy protocol ' + '{0}'.format(privacy_protocol)) self.authdata = snmp.UsmUserData( username, authKey=secret, privKey=secret, - authProtocol=snmp.usmHMACSHAAuthProtocol) + authProtocol=snmp.usmHMACSHAAuthProtocol, + privProtocol=privproto) self.eng = snmp.SnmpEngine() def walk(self, oid):