Skip to content

Commit ae3c352

Browse files
authored
feat: add simple address resolvers and examples (#1499)
1 parent 69f7c13 commit ae3c352

5 files changed

Lines changed: 192 additions & 12 deletions

File tree

examples/resolve_address.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env python
2+
3+
"""Example of resolving a name to an IP address."""
4+
5+
import asyncio
6+
import logging
7+
import sys
8+
9+
from zeroconf import AddressResolver, IPVersion
10+
from zeroconf.asyncio import AsyncZeroconf
11+
12+
13+
async def resolve_name(name: str) -> None:
14+
aiozc = AsyncZeroconf()
15+
await aiozc.zeroconf.async_wait_for_start()
16+
resolver = AddressResolver(name)
17+
if await resolver.async_request(aiozc.zeroconf, 3000):
18+
print(f"{name} IP addresses:", resolver.ip_addresses_by_version(IPVersion.All))
19+
else:
20+
print(f"Name {name} not resolved")
21+
await aiozc.async_close()
22+
23+
24+
if __name__ == "__main__":
25+
logging.basicConfig(level=logging.DEBUG)
26+
argv = sys.argv.copy()
27+
if "--debug" in argv:
28+
logging.getLogger("zeroconf").setLevel(logging.DEBUG)
29+
argv.remove("--debug")
30+
31+
if len(argv) < 2 or not argv[1]:
32+
raise ValueError("Usage: resolve_address.py [--debug] <name>")
33+
34+
name = argv[1]
35+
if not name.endswith("."):
36+
name += "."
37+
38+
asyncio.run(resolve_name(name))

src/zeroconf/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
from ._services.browser import ServiceBrowser
5959
from ._services.info import ( # noqa # import needed for backwards compat
6060
ServiceInfo,
61+
AddressResolver,
62+
AddressResolverIPv4,
63+
AddressResolverIPv6,
6164
instance_name_from_service_info,
6265
)
6366
from ._services.registry import ( # noqa # import needed for backwards compat

src/zeroconf/_services/info.pxd

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ from .._utils.ipaddress cimport (
2222
)
2323
from .._utils.time cimport current_time_millis
2424

25+
cdef cython.set _TYPE_AAAA_RECORDS
26+
cdef cython.set _TYPE_A_RECORDS
27+
cdef cython.set _TYPE_A_AAAA_RECORDS
2528

2629
cdef object _resolve_all_futures_to_none
2730

@@ -75,6 +78,7 @@ cdef class ServiceInfo(RecordUpdateListener):
7578
cdef public DNSText _dns_text_cache
7679
cdef public cython.list _dns_address_cache
7780
cdef public cython.set _get_address_and_nsec_records_cache
81+
cdef public cython.set _query_record_types
7882

7983
@cython.locals(record_update=RecordUpdate, update=bint, cache=DNSCache)
8084
cpdef void async_update_records(self, object zc, double now, cython.list records)
@@ -155,3 +159,12 @@ cdef class ServiceInfo(RecordUpdateListener):
155159
cdef double _get_initial_delay(self)
156160

157161
cdef double _get_random_delay(self)
162+
163+
cdef class AddressResolver(ServiceInfo):
164+
pass
165+
166+
cdef class AddressResolverIPv6(ServiceInfo):
167+
pass
168+
169+
cdef class AddressResolverIPv4(ServiceInfo):
170+
pass

src/zeroconf/_services/info.py

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@
8888
# the A/AAAA/SRV records for a host.
8989
_AVOID_SYNC_DELAY_RANDOM_INTERVAL = (20, 120)
9090

91+
_TYPE_AAAA_RECORDS = {_TYPE_AAAA}
92+
_TYPE_A_RECORDS = {_TYPE_A}
93+
_TYPE_A_AAAA_RECORDS = {_TYPE_A, _TYPE_AAAA}
94+
9195
bytes_ = bytes
9296
float_ = float
9397
int_ = int
@@ -146,6 +150,7 @@ class ServiceInfo(RecordUpdateListener):
146150
"_name",
147151
"_new_records_futures",
148152
"_properties",
153+
"_query_record_types",
149154
"host_ttl",
150155
"interface_index",
151156
"key",
@@ -210,6 +215,7 @@ def __init__(
210215
self._dns_service_cache: Optional[DNSService] = None
211216
self._dns_text_cache: Optional[DNSText] = None
212217
self._get_address_and_nsec_records_cache: Optional[Set[DNSRecord]] = None
218+
self._query_record_types = {_TYPE_SRV, _TYPE_TXT, _TYPE_A, _TYPE_AAAA}
213219

214220
@property
215221
def name(self) -> str:
@@ -917,18 +923,22 @@ def _generate_request_query(
917923
cache = zc.cache
918924
history = zc.question_history
919925
qu_question = question_type is QU_QUESTION
920-
self._add_question_with_known_answers(
921-
out, qu_question, history, cache, now, name, _TYPE_SRV, _CLASS_IN, True
922-
)
923-
self._add_question_with_known_answers(
924-
out, qu_question, history, cache, now, name, _TYPE_TXT, _CLASS_IN, True
925-
)
926-
self._add_question_with_known_answers(
927-
out, qu_question, history, cache, now, server, _TYPE_A, _CLASS_IN, False
928-
)
929-
self._add_question_with_known_answers(
930-
out, qu_question, history, cache, now, server, _TYPE_AAAA, _CLASS_IN, False
931-
)
926+
if _TYPE_SRV in self._query_record_types:
927+
self._add_question_with_known_answers(
928+
out, qu_question, history, cache, now, name, _TYPE_SRV, _CLASS_IN, True
929+
)
930+
if _TYPE_TXT in self._query_record_types:
931+
self._add_question_with_known_answers(
932+
out, qu_question, history, cache, now, name, _TYPE_TXT, _CLASS_IN, True
933+
)
934+
if _TYPE_A in self._query_record_types:
935+
self._add_question_with_known_answers(
936+
out, qu_question, history, cache, now, server, _TYPE_A, _CLASS_IN, False
937+
)
938+
if _TYPE_AAAA in self._query_record_types:
939+
self._add_question_with_known_answers(
940+
out, qu_question, history, cache, now, server, _TYPE_AAAA, _CLASS_IN, False
941+
)
932942
return out
933943

934944
def __repr__(self) -> str:
@@ -954,3 +964,45 @@ def __repr__(self) -> str:
954964

955965
class AsyncServiceInfo(ServiceInfo):
956966
"""An async version of ServiceInfo."""
967+
968+
969+
class AddressResolver(ServiceInfo):
970+
"""Resolve a host name to an IP address."""
971+
972+
def __init__(self, server: str) -> None:
973+
"""Initialize the AddressResolver."""
974+
super().__init__(server, server, server=server)
975+
self._query_record_types = _TYPE_A_AAAA_RECORDS
976+
977+
@property
978+
def _is_complete(self) -> bool:
979+
"""The ServiceInfo has all expected properties."""
980+
return bool(self._ipv4_addresses) or bool(self._ipv6_addresses)
981+
982+
983+
class AddressResolverIPv6(ServiceInfo):
984+
"""Resolve a host name to an IPv6 address."""
985+
986+
def __init__(self, server: str) -> None:
987+
"""Initialize the AddressResolver."""
988+
super().__init__(server, server, server=server)
989+
self._query_record_types = _TYPE_AAAA_RECORDS
990+
991+
@property
992+
def _is_complete(self) -> bool:
993+
"""The ServiceInfo has all expected properties."""
994+
return bool(self._ipv6_addresses)
995+
996+
997+
class AddressResolverIPv4(ServiceInfo):
998+
"""Resolve a host name to an IPv4 address."""
999+
1000+
def __init__(self, server: str) -> None:
1001+
"""Initialize the AddressResolver."""
1002+
super().__init__(server, server, server=server)
1003+
self._query_record_types = _TYPE_A_RECORDS
1004+
1005+
@property
1006+
def _is_complete(self) -> bool:
1007+
"""The ServiceInfo has all expected properties."""
1008+
return bool(self._ipv4_addresses)

tests/services/test_info.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1797,3 +1797,77 @@ async def test_service_info_nsec_records():
17971797
assert nsec_record.type == const._TYPE_NSEC
17981798
assert nsec_record.ttl == 50
17991799
assert nsec_record.rdtypes == [const._TYPE_A, const._TYPE_AAAA]
1800+
1801+
1802+
@pytest.mark.asyncio
1803+
async def test_address_resolver():
1804+
"""Test that the address resolver works."""
1805+
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
1806+
await aiozc.zeroconf.async_wait_for_start()
1807+
resolver = r.AddressResolver("address_resolver_test.local.")
1808+
resolve_task = asyncio.create_task(resolver.async_request(aiozc.zeroconf, 3000))
1809+
outgoing = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
1810+
outgoing.add_answer_at_time(
1811+
r.DNSAddress(
1812+
"address_resolver_test.local.",
1813+
const._TYPE_A,
1814+
const._CLASS_IN,
1815+
10000,
1816+
b"\x7f\x00\x00\x01",
1817+
),
1818+
0,
1819+
)
1820+
1821+
aiozc.zeroconf.async_send(outgoing)
1822+
assert await resolve_task
1823+
assert resolver.addresses == [b"\x7f\x00\x00\x01"]
1824+
1825+
1826+
@pytest.mark.asyncio
1827+
async def test_address_resolver_ipv4():
1828+
"""Test that the IPv4 address resolver works."""
1829+
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
1830+
await aiozc.zeroconf.async_wait_for_start()
1831+
resolver = r.AddressResolverIPv4("address_resolver_test_ipv4.local.")
1832+
resolve_task = asyncio.create_task(resolver.async_request(aiozc.zeroconf, 3000))
1833+
outgoing = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
1834+
outgoing.add_answer_at_time(
1835+
r.DNSAddress(
1836+
"address_resolver_test_ipv4.local.",
1837+
const._TYPE_A,
1838+
const._CLASS_IN,
1839+
10000,
1840+
b"\x7f\x00\x00\x01",
1841+
),
1842+
0,
1843+
)
1844+
1845+
aiozc.zeroconf.async_send(outgoing)
1846+
assert await resolve_task
1847+
assert resolver.addresses == [b"\x7f\x00\x00\x01"]
1848+
1849+
1850+
@pytest.mark.asyncio
1851+
@unittest.skipIf(not has_working_ipv6(), "Requires IPv6")
1852+
@unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled")
1853+
async def test_address_resolver_ipv6():
1854+
"""Test that the IPv6 address resolver works."""
1855+
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
1856+
await aiozc.zeroconf.async_wait_for_start()
1857+
resolver = r.AddressResolverIPv6("address_resolver_test_ipv6.local.")
1858+
resolve_task = asyncio.create_task(resolver.async_request(aiozc.zeroconf, 3000))
1859+
outgoing = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
1860+
outgoing.add_answer_at_time(
1861+
r.DNSAddress(
1862+
"address_resolver_test_ipv6.local.",
1863+
const._TYPE_AAAA,
1864+
const._CLASS_IN,
1865+
10000,
1866+
socket.inet_pton(socket.AF_INET6, "fe80::52e:c2f2:bc5f:e9c6"),
1867+
),
1868+
0,
1869+
)
1870+
1871+
aiozc.zeroconf.async_send(outgoing)
1872+
assert await resolve_task
1873+
assert resolver.ip_addresses_by_version(IPVersion.All) == [ip_address("fe80::52e:c2f2:bc5f:e9c6")]

0 commit comments

Comments
 (0)