From cfba7486777981f0a5739754aa8647cb6cec6ad8 Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 20 Oct 2026 17:16:44 +0700 Subject: [PATCH] fix: SPICE socket binding failure --- gns3server/compute/qemu/qemu_vm.py | 2 +- gns3server/utils/__init__.py | 17 ++++++++++++++--- tests/compute/qemu/test_qemu_vm.py | 2 +- tests/utils/test_ipv6.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 tests/utils/test_ipv6.py diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index f321fc21b..9d882953e 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -2064,7 +2064,7 @@ class QemuVM(KernelDatapathMixin, BaseNode): console_host = "::" except OSError as e: raise QemuError(f"Could not check if IPv6 is enabled: {e}") - return ["-spice", f"addr={console_host},port={port},disable-ticketing", "-vga", "qxl"] + return ["-spice", f"addr={console_host},port={port},disable-ticketing=on", "-vga", "qxl"] else: return [] diff --git a/gns3server/utils/__init__.py b/gns3server/utils/__init__.py index b1ba96c7d..b931bd2c8 100644 --- a/gns3server/utils/__init__.py +++ b/gns3server/utils/__init__.py @@ -99,15 +99,26 @@ def is_ipv6_enabled() -> bool: try: with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as sock: sock.bind(("::1", 0)) - return True except OSError as e: if e.errno in (errno.EADDRNOTAVAIL, errno.EAFNOSUPPORT): # EADDRNOTAVAIL is the errno if IPv6 modules/drivers are loaded but disabled. # EAFNOSUPPORT is the errno if IPv6 modules/drivers are not loaded at all. return False if e.errno == errno.EADDRINUSE: - return True - raise + pass + else: + raise + + # Applications like SPICE resolve the listen address with AI_ADDRCONFIG, + # which fails (EAI_ADDRFAMILY) for "::" when the IPv6 stack is loaded but no + # non-loopback IPv6 address is configured on the host. + try: + socket.getaddrinfo( + "::", None, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE | socket.AI_ADDRCONFIG + ) + except (socket.gaierror, UnicodeError): + return False + return True def md5sum(filename): diff --git a/tests/compute/qemu/test_qemu_vm.py b/tests/compute/qemu/test_qemu_vm.py index 66210e92f..da5293261 100644 --- a/tests/compute/qemu/test_qemu_vm.py +++ b/tests/compute/qemu/test_qemu_vm.py @@ -481,7 +481,7 @@ async def test_spice_option(vm, fake_qemu_img_binary): vm._console_type = "spice" vm._console = 5905 options = await vm._build_command() - assert "-spice addr=127.0.0.1,port=5905,disable-ticketing" in " ".join(options) + assert "-spice addr=127.0.0.1,port=5905,disable-ticketing=on" in " ".join(options) assert "-vga qxl" in " ".join(options) diff --git a/tests/utils/test_ipv6.py b/tests/utils/test_ipv6.py new file mode 100644 index 000000000..37ef88de8 --- /dev/null +++ b/tests/utils/test_ipv6.py @@ -0,0 +1,29 @@ +import socket +from unittest.mock import patch + +from gns3server.utils import is_ipv6_enabled + + +def test_is_ipv6_enabled_no_ipv6_support(): + + with patch("socket.has_ipv6", False): + assert is_ipv6_enabled() is False + + +def test_is_ipv6_enabled_no_configured_ipv6_address(): + """ + IPv6 stack loaded (binding ::1 works) but getaddrinfo with AI_ADDRCONFIG + fails, like SPICE does when no non-loopback IPv6 address is configured. + """ + + error = socket.gaierror( + getattr(socket, "EAI_ADDRFAMILY", socket.EAI_FAMILY), "Address family for hostname not supported" + ) + with patch("socket.has_ipv6", True), patch("socket.socket"), patch("socket.getaddrinfo", side_effect=error): + assert is_ipv6_enabled() is False + + +def test_is_ipv6_enabled(): + + with patch("socket.has_ipv6", True), patch("socket.socket"), patch("socket.getaddrinfo", return_value=[]): + assert is_ipv6_enabled() is True