Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions common/transport/DcgmIpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -770,19 +770,27 @@ void DcgmIpc::ConnectTcpAsyncImpl(DcgmIpcConnectTcp &tcpConnect)
bufferevent_setcb(bev, DcgmIpc::StaticReadCB, NULL, DcgmIpc::StaticEventCB, this);
bufferevent_enable(bev, EV_READ | EV_WRITE);

/* Allow IPv6 IPs to override family */
sa_family_t family = AF_INET;
/* Determine address family:
* - Bracketed or literal IPv6 address -> AF_INET6
* - Literal IPv4 address -> AF_INET
* - Hostname -> AF_UNSPEC so libevent resolves it to whichever family DNS returns */
sa_family_t family = AF_UNSPEC;
std::string &hostname = tcpConnect.m_hostname;
char buf[sizeof(struct in6_addr)];
memset(buf, 0, sizeof(buf));
if (hostname.size() >= 3 && hostname[0] == '[' && hostname[hostname.size() - 1] == ']')
{
hostname = hostname.substr(1, hostname.size() - 2);
family = AF_INET6;
}
char buf[16];
memset(buf, 0, sizeof(buf));
if (inet_pton(AF_INET6, hostname.c_str(), buf) > 0)
else if (inet_pton(AF_INET6, hostname.c_str(), buf) > 0)
{
family = AF_INET6;
}
else if (inet_pton(AF_INET, hostname.c_str(), buf) > 0)
{
family = AF_INET;
}

int ret = bufferevent_socket_connect_hostname(bev, m_dnsBase, family, hostname.c_str(), tcpConnect.m_port);
if (0 != ret)
Expand Down
8 changes: 8 additions & 0 deletions testing/python3/DcgmHandle.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import dcgm_structs
import dcgm_agent
import threading
import ipaddress


class DcgmHandle:
Expand Down Expand Up @@ -98,6 +99,13 @@ def __init__(self,
connectParams.persistAfterDisconnect = 0

if ipAddress is not None:
# Wrap bare IPv6 literals in brackets so ParseEndpoint can distinguish
# the address colons from the host:port separator (e.g. "::1" -> "[::1]").
try:
if ipaddress.ip_address(ipAddress).version == 6:
ipAddress = "[" + ipAddress + "]"
except ValueError:
pass # hostname or already-bracketed address — leave as-is
connectToAddress = "tcp://" + ipAddress
else:
connectToAddress = "unix://" + unixSocketPath
Expand Down
49 changes: 47 additions & 2 deletions testing/python3/tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_connection_disconnect_error_after_shutdown():
@test_utils.run_with_standalone_host_engine(passAppAsArg=True)
def test_dcgm_standalone_connection_disconnect_error_after_hostengine_terminate(handle, hostengineApp):
'''
Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after
Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after
the hostengine process is terminated via `nv-hostengine --term`.
'''

Expand All @@ -70,7 +70,7 @@ def test_dcgm_standalone_connection_disconnect_error_after_hostengine_terminate(
@test_utils.run_with_standalone_host_engine(passAppAsArg=True)
def test_dcgm_standalone_connection_disconnect_error_after_hostengine_murder(handle, hostengineApp):
'''
Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after
Test that DCGM_ST_CONNECTION_NOT_VALID is returned when the dcgm API is used after
the hostengine process is killed via a `SIGKILL` signal.
'''
handle = pydcgm.DcgmHandle(handle)
Expand Down Expand Up @@ -129,6 +129,51 @@ def test_dcgm_ipv6_loopback():
nvHe.validate()


@test_utils.run_only_as_root()
@test_utils.run_with_ipv6_enabled()
def test_dcgm_ipv6_hostname_resolution():
'''
Verify that a hostname which resolves exclusively to an IPv6 address can connect to a
host engine bound to [::1]. This exercises the AF_UNSPEC path in ConnectTcpAsyncImpl —
the fix for the bug where AF_INET was hardcoded, preventing hostname→IPv6 resolution.
The test is skipped when no IPv6-only hostname alias is available on the system.
'''
import socket as _socket

def _resolves_only_to_ipv6(hostname):
try:
if _socket.getaddrinfo(hostname, None, _socket.AF_INET):
return False
except _socket.gaierror:
pass
try:
return bool(_socket.getaddrinfo(hostname, None, _socket.AF_INET6))
except _socket.gaierror:
return False

# ip6-loopback / ip6-localhost are standard aliases for ::1 on many Linux distros
test_hostname = next(
(h for h in ('ip6-loopback', 'ip6-localhost') if _resolves_only_to_ipv6(h)),
None,
)
if test_hostname is None:
test_utils.skip_test("No hostname found that resolves exclusively to an IPv6 address")

nvHe = apps.NvHostEngineApp(['-b', '[::1]'])
nvHe.start(timeout=90)

handle = pydcgm.DcgmHandle(ipAddress=test_hostname, timeoutMs=90)
assert handle
dcgmSystem = handle.GetSystem()
dcgmSystem.discovery.GetAllGpuIds()

del handle
handle = None

nvHe.terminate()
nvHe.validate()


@test_utils.run_with_standalone_host_engine(20)
@test_utils.run_only_with_live_gpus()
def test_dcgm_connection_client_cleanup(handle, gpuIds):
Expand Down