From d62b58eee9f283ad1bf16390d487252d0b18c93e Mon Sep 17 00:00:00 2001 From: Lili Deng Date: Thu, 16 Jul 2026 15:41:26 +0800 Subject: [PATCH 1/4] perf(ntttcp): run ntttcp/lagscope over internal IPv4 on dual-stack ntttcp and lagscope bind/connect over IPv4 only. On use_ipv6 (dual-stack) environments server.internal_address is the IPv6 private address, so lagscope fails with 'no port opened for lagscope server' and every ntttcp perf attempt fails. Resolve the server's internal IPv4 from its primary NIC and use it as the ntttcp/lagscope target, so perf runs succeed even when the VMs are deployed with IPv6. IPv4-only deployments are unaffected (internal_address is already IPv4). --- .../testsuites/performance/common.py | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/lisa/microsoft/testsuites/performance/common.py b/lisa/microsoft/testsuites/performance/common.py index 1680ee475d..5edb6c03c8 100644 --- a/lisa/microsoft/testsuites/performance/common.py +++ b/lisa/microsoft/testsuites/performance/common.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. import inspect +import ipaddress import pathlib import time from functools import partial @@ -344,6 +345,28 @@ def perf_tcp_pps( notifier.notify(pps_message) +def _get_ntttcp_ipv4_address(node: RemoteNode) -> str: + # ntttcp and lagscope bind/connect over IPv4 only. When the environment is + # deployed dual-stack (use_ipv6), node.internal_address is the IPv6 private + # address, which makes lagscope fail to open its listening port. The node + # still has an internal IPv4 on the same (SRIOV) NIC, so run the perf tools + # over that IPv4 address instead. IPv4-only deployments are unaffected: the + # address is already IPv4 and is returned unchanged. + address = node.internal_address + try: + is_ipv6 = ipaddress.ip_address(address).version == 6 + except ValueError: + is_ipv6 = False + if not is_ipv6: + return address + ipv4_address = node.nics.get_primary_nic().ip_addr + node.log.debug( + f"internal address {address} is IPv6; ntttcp/lagscope will use the " + f"node's internal IPv4 {ipv4_address} instead." + ) + return ipv4_address + + def perf_ntttcp( # noqa: C901 test_result: TestResult, server: Optional[RemoteNode] = None, @@ -381,6 +404,11 @@ def perf_ntttcp( # noqa: C901 # if it's not filled, assume it's called by case directly. test_case_name = inspect.stack()[1][3] + # ntttcp and lagscope run over IPv4. On dual-stack (use_ipv6) environments + # server.internal_address is IPv6, so resolve the server's internal IPv4 to + # use as the ntttcp/lagscope target. + server_comm_address = _get_ntttcp_ipv4_address(server) + if connections is None: if udp_mode: connections = NTTTCP_UDP_CONCURRENCY @@ -535,7 +563,7 @@ def perf_ntttcp( # noqa: C901 ip=( lagscope_server_ip if lagscope_server_ip is not None - else server.internal_address + else server_comm_address ), no_debug_log=True, ) @@ -545,9 +573,7 @@ def perf_ntttcp( # noqa: C901 server_result = server_ntttcp.run_as_server_async( server_nic_name, server_ip=( - server.internal_address - if isinstance(server.os, BSD) - else "" + server_comm_address if isinstance(server.os, BSD) else "" ), ports_count=num_threads_p, buffer_size=buffer_size, @@ -562,7 +588,7 @@ def perf_ntttcp( # noqa: C901 # Start lagscope client to measure latency during the # ntttcp test client_lagscope_process = client_lagscope.run_as_client_async( - server_ip=server.internal_address, + server_ip=server_comm_address, ping_count=0, run_time_seconds=( run_time_seconds @@ -582,7 +608,7 @@ def perf_ntttcp( # noqa: C901 # Use daemon mode to run in background, then monitor process client_ntttcp_result = client_ntttcp.run_as_client( client_nic_name, - server.internal_address, + server_comm_address, buffer_size=buffer_size, threads_count=num_threads_n, ports_count=num_threads_p, From 64cef57b4cb46a16a0c8976a8a7fee1feefde224 Mon Sep 17 00:00:00 2001 From: Lili Deng Date: Mon, 27 Jul 2026 23:12:43 +0800 Subject: [PATCH 2/4] refactor(nic): move IPv4 resolution into Nics.get_internal_ipv4_address Move the dual-stack IPv4 fallback helper out of the ntttcp perf module and into Nics as a reusable get_internal_ipv4_address(): prefer the node's internal_address, fall back to the primary NIC's IPv4 when it is IPv6, and raise if the NIC has no IPv4 to fall back to. --- .../testsuites/performance/common.py | 25 +-------------- lisa/nic.py | 31 ++++++++++++++++++- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/lisa/microsoft/testsuites/performance/common.py b/lisa/microsoft/testsuites/performance/common.py index 5edb6c03c8..d9e262e628 100644 --- a/lisa/microsoft/testsuites/performance/common.py +++ b/lisa/microsoft/testsuites/performance/common.py @@ -1,7 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. import inspect -import ipaddress import pathlib import time from functools import partial @@ -345,28 +344,6 @@ def perf_tcp_pps( notifier.notify(pps_message) -def _get_ntttcp_ipv4_address(node: RemoteNode) -> str: - # ntttcp and lagscope bind/connect over IPv4 only. When the environment is - # deployed dual-stack (use_ipv6), node.internal_address is the IPv6 private - # address, which makes lagscope fail to open its listening port. The node - # still has an internal IPv4 on the same (SRIOV) NIC, so run the perf tools - # over that IPv4 address instead. IPv4-only deployments are unaffected: the - # address is already IPv4 and is returned unchanged. - address = node.internal_address - try: - is_ipv6 = ipaddress.ip_address(address).version == 6 - except ValueError: - is_ipv6 = False - if not is_ipv6: - return address - ipv4_address = node.nics.get_primary_nic().ip_addr - node.log.debug( - f"internal address {address} is IPv6; ntttcp/lagscope will use the " - f"node's internal IPv4 {ipv4_address} instead." - ) - return ipv4_address - - def perf_ntttcp( # noqa: C901 test_result: TestResult, server: Optional[RemoteNode] = None, @@ -407,7 +384,7 @@ def perf_ntttcp( # noqa: C901 # ntttcp and lagscope run over IPv4. On dual-stack (use_ipv6) environments # server.internal_address is IPv6, so resolve the server's internal IPv4 to # use as the ntttcp/lagscope target. - server_comm_address = _get_ntttcp_ipv4_address(server) + server_comm_address = server.nics.get_internal_ipv4_address() if connections is None: if udp_mode: diff --git a/lisa/nic.py b/lisa/nic.py index 6412ffc9b5..de7fceb845 100644 --- a/lisa/nic.py +++ b/lisa/nic.py @@ -8,7 +8,7 @@ from collections import OrderedDict from dataclasses import dataclass from pathlib import PurePosixPath -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast from assertpy import assert_that from retry import retry @@ -18,6 +18,7 @@ if TYPE_CHECKING: from lisa import Node + from lisa.node import RemoteNode class NicInfo: @@ -274,6 +275,34 @@ def get_nic_names(self) -> List[str]: def get_primary_nic(self) -> NicInfo: return self.get_nic_by_index(0) + def get_internal_ipv4_address(self) -> str: + # Return an IPv4 address usable for intra-VM communication. Prefer the + # node's internal_address, but on a dual-stack (use_ipv6) environment + # that address is IPv6 -- tools that bind/connect over IPv4 only (e.g. + # ntttcp/lagscope) cannot use it. The node still has an internal IPv4 + # on its primary (SRIOV) NIC, so fall back to that. IPv4-only + # deployments are unaffected: internal_address is already IPv4 and is + # returned unchanged. + address: str = cast("RemoteNode", self._node).internal_address + try: + is_ipv6 = ipaddress.ip_address(address).version == 6 + except ValueError: + is_ipv6 = False + if not is_ipv6: + return address + ipv4_address: str = self.get_primary_nic().ip_addr + if not ipv4_address: + raise LisaException( + f"internal address {address} is IPv6 but the primary NIC " + f"'{self.get_primary_nic().name}' has no IPv4 address to fall " + f"back to." + ) + self._node.log.debug( + f"internal address {address} is IPv6; using the node's internal " + f"IPv4 {ipv4_address} instead." + ) + return ipv4_address + def get_secondary_nic(self) -> NicInfo: # get a nic which isn't servicing the SSH connection with lisa. # will assert if none is present. From 92f32a992aae8557f8a0a7627c4c4b668386405b Mon Sep 17 00:00:00 2001 From: Lili Deng Date: Tue, 28 Jul 2026 11:15:11 +0800 Subject: [PATCH 3/4] perf: resolve internal IPv4 for all perf tools on dual-stack Apply the Nics.get_internal_ipv4_address() fallback to every perf tool that builds IPv4-style CLI args with no IPv6 bracket handling, so they work on dual-stack (use_ipv6) environments where internal_address is IPv6: lagscope (perf_tcp_latency), netperf (perf_tcp_pps), iperf3 (perf_iperf), and sockperf (perf_sockperf). --- .../testsuites/performance/common.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/lisa/microsoft/testsuites/performance/common.py b/lisa/microsoft/testsuites/performance/common.py index d9e262e628..a5d8a19ce0 100644 --- a/lisa/microsoft/testsuites/performance/common.py +++ b/lisa/microsoft/testsuites/performance/common.py @@ -251,12 +251,16 @@ def perf_tcp_latency(test_result: TestResult) -> List[NetworkLatencyPerformanceM server = cast(RemoteNode, environment.nodes[1]) client_lagscope = client.tools[Lagscope] server_lagscope = server.tools[Lagscope] + # lagscope builds IPv4-style CLI args (-s/-r) with no IPv6 bracket + # handling. On dual-stack (use_ipv6) environments server.internal_address is + # IPv6, so resolve the server's internal IPv4 to bind/connect over instead. + server_comm_address = server.nics.get_internal_ipv4_address() try: for lagscope in [client_lagscope, server_lagscope]: lagscope.set_busy_poll() - server_lagscope.run_as_server_async(ip=server.internal_address) + server_lagscope.run_as_server_async(ip=server_comm_address) latency_perf_messages = client_lagscope.create_latency_performance_messages( - client_lagscope.run_as_client(server_ip=server.internal_address), + client_lagscope.run_as_client(server_ip=server_comm_address), inspect.stack()[1][3], test_result, ) @@ -303,17 +307,22 @@ def perf_tcp_pps( ] ) + # netperf builds IPv4-style CLI args with no IPv6 bracket handling. On a + # dual-stack (use_ipv6) environment internal_address is IPv6, so resolve + # the internal IPv4 to bind/target instead. + server_comm_ip = server_node.nics.get_internal_ipv4_address() server_interface_ip: str = "" client_interface_ip: str = "" if use_internal_address: - assert_that(server_node.internal_address).described_as( + assert_that(server_comm_ip).described_as( "Server Node: internal address is not set" ).is_not_empty() - assert_that(client_node.internal_address).described_as( + client_comm_ip = client_node.nics.get_internal_ipv4_address() + assert_that(client_comm_ip).described_as( "Client Node: internal address is not set" ).is_not_empty() - server_interface_ip = server_node.internal_address - client_interface_ip = client_node.internal_address + server_interface_ip = server_comm_ip + client_interface_ip = client_comm_ip cpu = client_node.tools[Lscpu] thread_count = cpu.get_thread_count() @@ -326,10 +335,10 @@ def perf_tcp_pps( for port in ports: server_netperf.run_as_server(port, interface_ip=server_interface_ip) for port in ports: - # Use server.internal_address as target since netperf client needs + # Use the server's internal IPv4 as target since netperf client needs # the server's IP (which may differ from the interface it binds to) client_netperf.run_as_client_async( - server_ip=server_node.internal_address, + server_ip=server_comm_ip, core_count=thread_count, port=port, interface_ip=client_interface_ip, @@ -784,11 +793,15 @@ def perf_iperf( ) test_case_name = inspect.stack()[1][3] iperf3_messages_list: List[Any] = [] + # iperf3 is invoked with ip_version="4"; on a dual-stack (use_ipv6) + # environment internal_address is IPv6, so resolve the server's internal + # IPv4 to use as the client target / bind interface. + server_comm_ip = server.nics.get_internal_ipv4_address() server_interface_ip = "" client_interface_ip = "" if run_with_internal_address: - server_interface_ip = server.internal_address - client_interface_ip = client.internal_address + server_interface_ip = server_comm_ip + client_interface_ip = client.nics.get_internal_ipv4_address() assert server_interface_ip, "Server Node: internal address is not set" assert client_interface_ip, "Client Node: internal address is not set" @@ -834,7 +847,7 @@ def perf_iperf( current_client_iperf_instances += 1 client_iperf3_process_list.append( client_iperf3.run_as_client_async( - server.internal_address, + server_comm_ip, output_json=True, report_periodic=1, report_unit="g", @@ -908,7 +921,9 @@ def perf_sockperf( "sockperf: Warmup stage", timeout=30, ) - client_output = client.tools[Sockperf].run_client(mode, server.internal_address) + client_output = client.tools[Sockperf].run_client( + mode, server.nics.get_internal_ipv4_address() + ) client.tools[Sockperf].create_latency_performance_message( client_output, test_case_name, test_result ) From e3c763b561b72368ea8590aa173e5fa46e6e314a Mon Sep 17 00:00:00 2001 From: Lili Deng Date: Tue, 28 Jul 2026 12:58:02 +0800 Subject: [PATCH 4/4] fix(nic): safely resolve internal IPv4, tidy netperf assert get_internal_ipv4_address() now reads internal_address via getattr so it never raises AttributeError on nodes without it (e.g. LocalNode); it prefers internal_address only when it's a valid IPv4 and otherwise falls back to the primary NIC IPv4 (raising if none). Also assert on the netperf interface_ip variables directly in perf_tcp_pps. --- .../testsuites/performance/common.py | 9 +++--- lisa/nic.py | 32 +++++++++---------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/lisa/microsoft/testsuites/performance/common.py b/lisa/microsoft/testsuites/performance/common.py index a5d8a19ce0..24e7eef5b8 100644 --- a/lisa/microsoft/testsuites/performance/common.py +++ b/lisa/microsoft/testsuites/performance/common.py @@ -314,15 +314,14 @@ def perf_tcp_pps( server_interface_ip: str = "" client_interface_ip: str = "" if use_internal_address: - assert_that(server_comm_ip).described_as( + server_interface_ip = server_comm_ip + client_interface_ip = client_node.nics.get_internal_ipv4_address() + assert_that(server_interface_ip).described_as( "Server Node: internal address is not set" ).is_not_empty() - client_comm_ip = client_node.nics.get_internal_ipv4_address() - assert_that(client_comm_ip).described_as( + assert_that(client_interface_ip).described_as( "Client Node: internal address is not set" ).is_not_empty() - server_interface_ip = server_comm_ip - client_interface_ip = client_comm_ip cpu = client_node.tools[Lscpu] thread_count = cpu.get_thread_count() diff --git a/lisa/nic.py b/lisa/nic.py index de7fceb845..0cae78a031 100644 --- a/lisa/nic.py +++ b/lisa/nic.py @@ -8,7 +8,7 @@ from collections import OrderedDict from dataclasses import dataclass from pathlib import PurePosixPath -from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional from assertpy import assert_that from retry import retry @@ -18,7 +18,6 @@ if TYPE_CHECKING: from lisa import Node - from lisa.node import RemoteNode class NicInfo: @@ -277,29 +276,28 @@ def get_primary_nic(self) -> NicInfo: def get_internal_ipv4_address(self) -> str: # Return an IPv4 address usable for intra-VM communication. Prefer the - # node's internal_address, but on a dual-stack (use_ipv6) environment - # that address is IPv6 -- tools that bind/connect over IPv4 only (e.g. - # ntttcp/lagscope) cannot use it. The node still has an internal IPv4 - # on its primary (SRIOV) NIC, so fall back to that. IPv4-only - # deployments are unaffected: internal_address is already IPv4 and is - # returned unchanged. - address: str = cast("RemoteNode", self._node).internal_address + # node's internal_address when it is a valid IPv4. On a dual-stack + # (use_ipv6) environment it is IPv6, and some node types (e.g. + # LocalNode) don't define internal_address at all -- in both cases fall + # back to the primary NIC's internal IPv4, since tools that bind/connect + # over IPv4 only (ntttcp/lagscope/netperf/sockperf) need an IPv4 target. + address: str = getattr(self._node, "internal_address", "") or "" try: - is_ipv6 = ipaddress.ip_address(address).version == 6 + is_ipv4 = ipaddress.ip_address(address).version == 4 except ValueError: - is_ipv6 = False - if not is_ipv6: + is_ipv4 = False + if is_ipv4: return address ipv4_address: str = self.get_primary_nic().ip_addr if not ipv4_address: raise LisaException( - f"internal address {address} is IPv6 but the primary NIC " - f"'{self.get_primary_nic().name}' has no IPv4 address to fall " - f"back to." + f"internal address '{address}' is not a usable IPv4 and the " + f"primary NIC '{self.get_primary_nic().name}' has no IPv4 " + f"address to fall back to." ) self._node.log.debug( - f"internal address {address} is IPv6; using the node's internal " - f"IPv4 {ipv4_address} instead." + f"internal address '{address}' is not IPv4; using the node's " + f"internal IPv4 {ipv4_address} instead." ) return ipv4_address