From 92ff5849d37e55b7632f6233c7e721d0eae49fa9 Mon Sep 17 00:00:00 2001 From: S Karthik <168535924+Skarthik06@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:01:02 +0530 Subject: [PATCH 1/2] netbios: support a list of names in nbns_resolve The cache key introduced in #5115 contained the list (or bytearray) as is, which raised TypeError: unhashable type. A list now resolves each name separately and returns a dict, and a bytearray is converted to bytes. AI-Assisted: yes (Claude Sonnet 5.5) --- scapy/layers/netbios.py | 18 ++++- test/scapy/layers/netbios.uts | 131 ++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 3 deletions(-) diff --git a/scapy/layers/netbios.py b/scapy/layers/netbios.py index 8335d99dbb9..089e1b27882 100644 --- a/scapy/layers/netbios.py +++ b/scapy/layers/netbios.py @@ -41,6 +41,7 @@ # Typing imports from typing import ( + Dict, List, Union, ) @@ -435,24 +436,35 @@ def tcp_reassemble(cls, data, *args, **kwargs): @conf.commands.register def nbns_resolve( - qname: str, + qname: Union[str, bytes, bytearray, List[Union[str, bytes]]], iface: Union[_GlobInterfaceType, List[_GlobInterfaceType]] = None, raw: bool = False, timeout: int = 3, **kwargs, -) -> List[str]: +) -> Union[List[str], Dict[Union[str, bytes], List[str]]]: """ Perform a simple NBNS (NetBios Name Services) resolution with caching - :param qname: the name to query + :param qname: the name to query. If a list of names is given, each name is + resolved independently and a dict {name: result} is returned. :param iface: the interfaces to use. (default: all) :param raw: return the whole netbios packet (default False) :param timeout: seconds until timeout (per server) :raise TimeoutError: if no DNS servers were reached in time. """ + if isinstance(qname, list): + # One independent (and cached) resolution per name + return { + name: nbns_resolve(name, iface=iface, raw=raw, timeout=timeout, **kwargs) + for name in qname + } + kwargs.setdefault("verbose", 0) # Unify types (for caching) + if isinstance(qname, bytearray): + # Mutable, so it can't be used in the cache key + qname = bytes(qname) qname = NBNSQueryRequest.QUESTION_NAME.any2i(None, qname) # Check cache diff --git a/test/scapy/layers/netbios.uts b/test/scapy/layers/netbios.uts index 9c17ae460ee..21681391869 100644 --- a/test/scapy/layers/netbios.uts +++ b/test/scapy/layers/netbios.uts @@ -166,3 +166,134 @@ try: assert _nbns_queries == [b"ALPHA", b"BETA"] finally: _nbns_cache.flush() + += nbns_resolve accepts a list of names +~ mock + +from unittest import mock +from scapy.layers.netbios import _nbns_cache, nbns_resolve + +_nbns_queries = [] +_nbns_cache.flush() + +def _answer_nbns(requests, **kwargs): + packet = requests[0] if isinstance(requests, list) else requests + name = packet.QUESTION_NAME + _nbns_queries.append(name) + address = "203.0.113.66" if name == b"ALPHA" else "192.0.2.44" + return NBNSHeader(RESPONSE=1, NM_FLAGS=0x50, ANCOUNT=1) / NBNSQueryResponse( + RR_NAME=name, + ADDR_ENTRY=[NBNS_ADD_ENTRY(NB_ADDRESS=address)], + ) + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + result = nbns_resolve(["ALPHA", "BETA"], iface="test") + assert result == {"ALPHA": ["203.0.113.66"], "BETA": ["192.0.2.44"]} + assert _nbns_queries == [b"ALPHA", b"BETA"] +finally: + _nbns_cache.flush() + += nbns_resolve with an empty list sends nothing +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + assert nbns_resolve([], iface="test") == {} + assert _nbns_queries == [] +finally: + _nbns_cache.flush() + += nbns_resolve with a bytearray name does not break the cache +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + assert nbns_resolve(bytearray(b"ALPHA"), iface="test") == ["203.0.113.66"] + # Same logical name as a str and as bytes: served from the cache + assert nbns_resolve("ALPHA", iface="test") == ["203.0.113.66"] + assert nbns_resolve(b"ALPHA", iface="test") == ["203.0.113.66"] + assert _nbns_queries == [b"ALPHA"] +finally: + _nbns_cache.flush() + += nbns_resolve single names keep returning a list +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + assert nbns_resolve("ALPHA", iface="test") == ["203.0.113.66"] + assert nbns_resolve(b"BETA", iface="test") == ["192.0.2.44"] +finally: + _nbns_cache.flush() + += nbns_resolve with a list reuses the cache and keeps names apart +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + assert nbns_resolve("ALPHA", iface="test") == ["203.0.113.66"] + result = nbns_resolve(["ALPHA", "BETA"], iface="test") + assert result == {"ALPHA": ["203.0.113.66"], "BETA": ["192.0.2.44"]} + # Second list call: everything is cached + assert nbns_resolve(["BETA", "ALPHA"], iface="test") == result + # ALPHA was cached by the first call, so it is queried once overall + assert _nbns_queries == [b"ALPHA", b"BETA"] +finally: + _nbns_cache.flush() + += nbns_resolve with a list and raw=True returns one packet per name +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + result = nbns_resolve(["ALPHA", "BETA"], iface="test", raw=True) + assert result["ALPHA"][NBNSQueryResponse].ADDR_ENTRY[0].NB_ADDRESS == "203.0.113.66" + assert result["BETA"][NBNSQueryResponse].ADDR_ENTRY[0].NB_ADDRESS == "192.0.2.44" +finally: + _nbns_cache.flush() + += nbns_resolve with a list raises TimeoutError when a name is unanswered +~ mock + +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", return_value=None), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + try: + nbns_resolve(["ALPHA", "BETA"], iface="test") + raised = False + except TimeoutError: + raised = True + assert raised +finally: + _nbns_cache.flush() From 993149fed22f7581bdaa59a8433ba06cab8d864c Mon Sep 17 00:00:00 2001 From: S Karthik <168535924+Skarthik06@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:10:34 +0530 Subject: [PATCH 2/2] netbios: test duplicate names, raw results and timeouts in nbns_resolve Cover a list with duplicate names, raw and default results of the same name, and names cached before a timeout in a list. AI-Assisted: yes (Claude Sonnet 5.5) --- test/scapy/layers/netbios.uts | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/scapy/layers/netbios.uts b/test/scapy/layers/netbios.uts index 21681391869..730576b55ca 100644 --- a/test/scapy/layers/netbios.uts +++ b/test/scapy/layers/netbios.uts @@ -297,3 +297,66 @@ try: assert raised finally: _nbns_cache.flush() + += nbns_resolve with duplicate names in a list queries each name once +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + result = nbns_resolve(["ALPHA", "ALPHA"], iface="test") + assert result == {"ALPHA": ["203.0.113.66"]} + assert _nbns_queries == [b"ALPHA"] +finally: + _nbns_cache.flush() + += nbns_resolve keeps raw and default results of a name apart +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +try: + with mock.patch("scapy.layers.netbios.sr1", side_effect=_answer_nbns), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + assert nbns_resolve("ALPHA", iface="test") == ["203.0.113.66"] + assert isinstance(nbns_resolve("ALPHA", iface="test", raw=True), NBNSHeader) + assert _nbns_queries == [b"ALPHA", b"ALPHA"] +finally: + _nbns_cache.flush() + += nbns_resolve with a list keeps the names resolved before a timeout cached +~ mock + +_nbns_queries.clear() +_nbns_cache.flush() + +def _answer_nbns_or_none(requests, **kwargs): + packet = requests[0] if isinstance(requests, list) else requests + if packet.QUESTION_NAME == b"BETA": + _nbns_queries.append(packet.QUESTION_NAME) + return None + return _answer_nbns(requests, **kwargs) + +try: + with mock.patch( + "scapy.layers.netbios.sr1", side_effect=_answer_nbns_or_none + ), mock.patch( + "scapy.layers.netbios.conf.route.get_if_bcast", return_value=["192.0.2.255"] + ): + try: + nbns_resolve(["ALPHA", "BETA"], iface="test") + raised = False + except TimeoutError: + raised = True + assert raised + # ALPHA was cached by the failed call: no new query for it + assert nbns_resolve("ALPHA", iface="test") == ["203.0.113.66"] + assert _nbns_queries == [b"ALPHA", b"BETA"] +finally: + _nbns_cache.flush()