From 63cc1fd4ec2cfae0ed3b7e2c6ca0b2687aabae4d Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Thu, 10 Apr 2025 14:43:48 +0200 Subject: [PATCH 1/2] fix: avoid loading adapter list twice Load adapter list only once if operating in dual-stack mode. Also fix typing around adapater list to match what ifaddr defines. --- src/zeroconf/_utils/net.py | 27 ++++++++++++++++++--------- tests/utils/test_net.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/zeroconf/_utils/net.py b/src/zeroconf/_utils/net.py index b4f3ef77..9c6ec63f 100644 --- a/src/zeroconf/_utils/net.py +++ b/src/zeroconf/_utils/net.py @@ -28,7 +28,7 @@ import socket import struct import sys -from collections.abc import Sequence +from collections.abc import Iterable, Sequence from typing import Any, Union, cast import ifaddr @@ -73,19 +73,27 @@ def _encode_address(address: str) -> bytes: return socket.inet_pton(address_family, address) -def get_all_addresses() -> list[str]: - return list({addr.ip for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv4}) # type: ignore[misc] +def get_all_addresses_ipv4(adapters: Iterable[ifaddr.Adapter]) -> list[str]: + return list({addr.ip for iface in adapters for addr in iface.ips if addr.is_IPv4}) # type: ignore[misc] -def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: +def get_all_addresses_ipv6(adapters: Iterable[ifaddr.Adapter]) -> list[tuple[tuple[str, int, int], int]]: # IPv6 multicast uses positive indexes for interfaces # TODO: What about multi-address interfaces? return list( - {(addr.ip, iface.index) for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv6} # type: ignore[misc] + {(addr.ip, iface.index) for iface in adapters for addr in iface.ips if addr.is_IPv6} # type: ignore[misc] ) -def ip6_to_address_and_index(adapters: list[ifaddr.Adapter], ip: str) -> tuple[tuple[str, int, int], int]: +def get_all_addresses() -> list[str]: # required for backwards compat + return get_all_addresses_ipv4(ifaddr.get_adapters()) + + +def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: # required for backwards compat + return get_all_addresses_ipv6(ifaddr.get_adapters()) + + +def ip6_to_address_and_index(adapters: Iterable[ifaddr.Adapter], ip: str) -> tuple[tuple[str, int, int], int]: if "%" in ip: ip = ip[: ip.index("%")] # Strip scope_id. ipaddr = ipaddress.ip_address(ip) @@ -102,7 +110,7 @@ def ip6_to_address_and_index(adapters: list[ifaddr.Adapter], ip: str) -> tuple[t raise RuntimeError(f"No adapter found for IP address {ip}") -def interface_index_to_ip6_address(adapters: list[ifaddr.Adapter], index: int) -> tuple[str, int, int]: +def interface_index_to_ip6_address(adapters: Iterable[ifaddr.Adapter], index: int) -> tuple[str, int, int]: for adapter in adapters: if adapter.index == index: for adapter_ip in adapter.ips: @@ -152,10 +160,11 @@ def normalize_interface_choice( if ip_version != IPVersion.V6Only: result.append("0.0.0.0") elif choice is InterfaceChoice.All: + adapters = ifaddr.get_adapters() if ip_version != IPVersion.V4Only: - result.extend(get_all_addresses_v6()) + result.extend(get_all_addresses_ipv6(adapters)) if ip_version != IPVersion.V6Only: - result.extend(get_all_addresses()) + result.extend(get_all_addresses_ipv4(adapters)) if not result: raise RuntimeError( f"No interfaces to listen on, check that any interfaces have IP version {ip_version}" diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index 6bdafb37..95b50217 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -35,6 +35,32 @@ def _generate_mock_adapters(): return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] +def test_get_all_addresses(): + """Test public get_all_addresses API.""" + with patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + from zeroconf import get_all_addresses + + addresses = get_all_addresses() + assert isinstance(addresses, list) + assert len(addresses) == 3 + + +def test_get_all_addresses_v6(): + """Test public get_all_addresses_v6 API.""" + with patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + from zeroconf import get_all_addresses_v6 + + addresses = get_all_addresses_v6() + assert isinstance(addresses, list) + assert len(addresses) == 1 + + def test_ip6_to_address_and_index(): """Test we can extract from mocked adapters.""" adapters = _generate_mock_adapters() @@ -84,8 +110,8 @@ def test_ip6_addresses_to_indexes(): def test_normalize_interface_choice_errors(): """Test we generate exception on invalid input.""" with ( - patch("zeroconf._utils.net.get_all_addresses", return_value=[]), - patch("zeroconf._utils.net.get_all_addresses_v6", return_value=[]), + patch("zeroconf._utils.net.get_all_addresses_ipv4", return_value=[]), + patch("zeroconf._utils.net.get_all_addresses_ipv6", return_value=[]), pytest.raises(RuntimeError), ): netutils.normalize_interface_choice(r.InterfaceChoice.All) From e64d166e8c399b4b50166bbff4474faef5756bcb Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 14 Apr 2025 09:58:01 +0200 Subject: [PATCH 2/2] chore: deprecate get_all_addresses and get_all_addresses_v6 Officially deprecate get_all_addresses and get_all_addresses_v6 in favor of using ifaddr directly. --- src/zeroconf/_utils/net.py | 17 +++++++++++++++-- tests/utils/test_net.py | 34 ++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/zeroconf/_utils/net.py b/src/zeroconf/_utils/net.py index 9c6ec63f..e687ab60 100644 --- a/src/zeroconf/_utils/net.py +++ b/src/zeroconf/_utils/net.py @@ -28,6 +28,7 @@ import socket import struct import sys +import warnings from collections.abc import Iterable, Sequence from typing import Any, Union, cast @@ -85,11 +86,23 @@ def get_all_addresses_ipv6(adapters: Iterable[ifaddr.Adapter]) -> list[tuple[tup ) -def get_all_addresses() -> list[str]: # required for backwards compat +def get_all_addresses() -> list[str]: + warnings.warn( + "get_all_addresses is deprecated, and will be removed in a future version. Use ifaddr" + "directly instead to get a list of adapters.", + DeprecationWarning, + stacklevel=2, + ) return get_all_addresses_ipv4(ifaddr.get_adapters()) -def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: # required for backwards compat +def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: + warnings.warn( + "get_all_addresses_v6 is deprecated, and will be removed in a future version. Use ifaddr" + "directly instead to get a list of adapters.", + DeprecationWarning, + stacklevel=2, + ) return get_all_addresses_ipv6(ifaddr.get_adapters()) diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index 95b50217..eff2befd 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -6,12 +6,14 @@ import socket import sys import unittest +import warnings from unittest.mock import MagicMock, Mock, patch import ifaddr import pytest import zeroconf as r +from zeroconf import get_all_addresses, get_all_addresses_v6 from zeroconf._utils import net as netutils @@ -35,30 +37,38 @@ def _generate_mock_adapters(): return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] -def test_get_all_addresses(): +def test_get_all_addresses() -> None: """Test public get_all_addresses API.""" - with patch( - "zeroconf._utils.net.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + warnings.catch_warnings(record=True) as warned, ): - from zeroconf import get_all_addresses - addresses = get_all_addresses() assert isinstance(addresses, list) assert len(addresses) == 3 + assert len(warned) == 1 + first_warning = warned[0] + assert "get_all_addresses is deprecated" in str(first_warning.message) -def test_get_all_addresses_v6(): +def test_get_all_addresses_v6() -> None: """Test public get_all_addresses_v6 API.""" - with patch( - "zeroconf._utils.net.ifaddr.get_adapters", - return_value=_generate_mock_adapters(), + with ( + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + warnings.catch_warnings(record=True) as warned, ): - from zeroconf import get_all_addresses_v6 - addresses = get_all_addresses_v6() assert isinstance(addresses, list) assert len(addresses) == 1 + assert len(warned) == 1 + first_warning = warned[0] + assert "get_all_addresses_v6 is deprecated" in str(first_warning.message) def test_ip6_to_address_and_index():