Repository: postlund/pyatv

Issue:
The wrong address is obtained from AirPort Express devices
<!--
READ THIS FIRST!

Thank you for reporting any bugs you find! Before opening a new issue, please
look through already open issues (someone else might have reported it already).
Also, take a look in the FAQ and especially the *Known Issues* section at the
bottom. Here is a direct link:

https://pyatv.dev/support/faq/
-->

**Describe the bug**
The AirPort Express isn't officially supported yet, but I wanted to try it out and see what happens. For one thing, something is up with the MDNS scanning because an address from the 169.254.0.0/16 space is picked up. I guess it has something to do with auto configuration. It causes problems for `pyatv` though.

**To Reproduce**
Scanning shows incorrect IP address (correct is 10.0.10.194):
```
$ atvremote scan
Scan Results
========================================
       Name: Pierre's AirPort Express
   Model/SW: Unknown Model Unknown OS
    Address: 169.254.81.198
        MAC: 48:D7:XX:XX:XX:XX
 Deep Sleep: False
Identifiers:
 - 48:D7:XX:XX:XX:XX
 - 48D7XXXXXXXX
Services:
 - Protocol: AirPlay, Port: 7000, Credentials: None
 - Protocol: RAOP, Port: 7000, Credentials: None
```

Unicast scanning won't work as it will override the address when device is found:
```
$ atvremote -s 10.0.10.194 --debug stream_file=sample_2mb.mp3
2021-05-17 12:22:11 DEBUG [pyatv.support.knock]: Knocking at ports [3689, 7000, 49152, 32498] on 10.0.10.194
2021-05-17 12:22:11 DEBUG [pyatv.support.scan]: Auto-discovered Pierre's AirPort Express at 169.254.81.198:7000 (Protocol.AirPlay)
2021-05-17 12:22:11 DEBUG [pyatv.support.scan]: Auto-discovered Pierre's AirPort Express at 169.254.81.198:7000 (Protocol.RAOP)
2021-05-17 12:22:11 INFO [root]: Auto-discovered Pierre's AirPort Express at 169.254.81.198
2021-05-17 12:22:11 DEBUG [pyatv.airplay]: No AirPlay credentials loaded
2021-05-17 12:22:13 DEBUG [pyatv.support.knock]: Knocking at ports [3689, 7000, 49152, 32498] on 10.0.10.194
...
```
**Expected behavior**

**System Setup (please complete the following information):**
 - OS: any
 - Python: any
 - pyatv: master
 - Apple TV: AirPort Express 2ng gen

**Additional context**
I guess ignoring the address would be a solution for now.


Discussion:
gitpod-io[bot]: 

<a href="https://gitpod.io/#https://github.com/postlund/pyatv/pull/1095"><img src="https://gitpod.io/button/open-in-gitpod.svg"/></a>


codecov[bot]: # [Codecov](https://codecov.io/gh/postlund/pyatv/pull/1095?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) Report
> Merging [#1095](https://codecov.io/gh/postlund/pyatv/pull/1095?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) (a7ff968) into [master](https://codecov.io/gh/postlund/pyatv/commit/9393b510af9c3fdce92ef5b104f2c461fde168d0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) (9393b51) will **increase** coverage by `0.05%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/postlund/pyatv/pull/1095/graphs/tree.svg?width=650&height=150&src=pr&token=wb6azJjRSY&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)](https://codecov.io/gh/postlund/pyatv/pull/1095?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)

```diff
@@            Coverage Diff             @@
##           master    #1095      +/-   ##
==========================================
+ Coverage   88.74%   88.79%   +0.05%     
==========================================
  Files          57       57              
  Lines        5384     5392       +8     
==========================================
+ Hits         4778     4788      +10     
+ Misses        606      604       -2     
```


| [Impacted Files](https://codecov.io/gh/postlund/pyatv/pull/1095?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) | Coverage Δ | |
|---|---|---|
| [pyatv/support/mdns.py](https://codecov.io/gh/postlund/pyatv/pull/1095/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvc3VwcG9ydC9tZG5zLnB5) | `88.46% <100.00%> (+0.40%)` | :arrow_up: |
| [pyatv/mrp/connection.py](https://codecov.io/gh/postlund/pyatv/pull/1095/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvbXJwL2Nvbm5lY3Rpb24ucHk=) | `81.25% <0.00%> (-2.09%)` | :arrow_down: |
| [pyatv/mrp/protocol.py](https://codecov.io/gh/postlund/pyatv/pull/1095/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvbXJwL3Byb3RvY29sLnB5) | `97.79% <0.00%> (+1.47%)` | :arrow_up: |
| [pyatv/support/scan.py](https://codecov.io/gh/postlund/pyatv/pull/1095/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvc3VwcG9ydC9zY2FuLnB5) | `98.03% <0.00%> (+1.96%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/postlund/pyatv/pull/1095?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/postlund/pyatv/pull/1095?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl). Last update [9393b51...a7ff968](https://codecov.io/gh/postlund/pyatv/pull/1095?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).


Patch:
diff --git a/pyatv/support/mdns.py b/pyatv/support/mdns.py
index b4213b37..3bd2d96d 100644
--- a/pyatv/support/mdns.py
+++ b/pyatv/support/mdns.py
@@ -83,9 +83,15 @@ def _get_model(services: typing.List[Service]) -> typing.Optional[str]:
     return None
 
 
-def parse_services(message: DnsMessage) -> typing.List[Service]:
+def _first_rd(qtype: QueryType, entries: typing.Dict[int, typing.List[DnsResource]]):
+    return entries[qtype][0].rd if qtype in entries else None
+
+
+def parse_services(  # pylint: disable=too-many-locals
+    message: DnsMessage,
+) -> typing.List[Service]:
     """Parse DNS response into Service objects."""
-    table: typing.Dict[str, typing.Dict[int, DnsResource]] = {}
+    table: typing.Dict[str, typing.Dict[int, typing.List[DnsResource]]] = {}
     ptrs: typing.Dict[str, str] = {}  # qname -> real name
     results: typing.Dict[str, Service] = {}
 
@@ -94,7 +100,10 @@ def parse_services(message: DnsMessage) -> typing.List[Service]:
         if record.qtype == QueryType.PTR and record.qname.startswith("_"):
             ptrs[record.qname] = record.rd
         else:
-            table.setdefault(record.qname, {})[record.qtype] = record
+            entry = table.setdefault(record.qname, {})
+            if record.qtype not in entry:
+                entry[record.qtype] = []
+            entry[record.qtype].append(record)
 
     # Build services
     for service, device in table.items():
@@ -103,19 +112,24 @@ def parse_services(message: DnsMessage) -> typing.List[Service]:
         except ValueError:
             continue
 
-        port = device[QueryType.SRV].rd["port"] if QueryType.SRV in device else 0
-        target = device[QueryType.SRV].rd["target"] if QueryType.SRV in device else None
-        properties = device[QueryType.TXT].rd if QueryType.TXT in device else {}
+        srv_rd = _first_rd(QueryType.SRV, device)
+        target = srv_rd["target"] if srv_rd else None
+
+        target_records = table.get(typing.cast(str, target), {}).get(QueryType.A, [])
+        address = None
 
-        target_record = table.get(typing.cast(str, target), {}).get(QueryType.A)
-        address = IPv4Address(target_record.rd) if target_record else None
+        # Pick one address that is not link-local
+        for addr in [IPv4Address(record.rd) for record in target_records]:
+            if not addr.is_link_local:
+                address = addr
+                break
 
         results[service] = Service(
             service_name.ptr_name,
             typing.cast(str, service_name.instance),
             address,
-            port,
-            _decode_properties(properties),
+            srv_rd["port"] if srv_rd else 0,
+            _decode_properties(_first_rd(QueryType.TXT, device) or {}),
         )
 
     # If there are PTRs to unknown services, create placeholders


Test patch:
diff --git a/tests/fake_udns.py b/tests/fake_udns.py
index a6d54183..8be57559 100644
--- a/tests/fake_udns.py
+++ b/tests/fake_udns.py
@@ -2,9 +2,10 @@ import asyncio
 from collections import namedtuple
 from contextlib import contextmanager
 from ipaddress import IPv4Address
+from itertools import chain
 import logging
 import struct
-from typing import Dict, List, Optional, Tuple, Union, cast
+from typing import Dict, List, Mapping, NamedTuple, Optional, Tuple, Union, cast
 from unittest.mock import patch
 
 from pyatv.support import dns, mdns
@@ -13,20 +14,28 @@ from tests.support import dns_utils
 
 _LOGGER = logging.getLogger(__name__)
 
-FakeDnsService = namedtuple("FakeDnsService", "name address port properties model")
+
+class FakeDnsService(NamedTuple):
+    """Representation of fake DNS service."""
+
+    name: str
+    addresses: List[str]
+    port: int
+    properties: Mapping[str, str]
+    model: Optional[str]
 
 
 def mrp_service(
     service_name: str,
     atv_name: str,
     identifier: str,
-    address="127.0.0.1",
+    addresses: List[str] = ["127.0.0.1"],
     port: int = 49152,
     model: Optional[str] = None,
 ) -> Tuple[str, FakeDnsService]:
     service = FakeDnsService(
         name=service_name,
-        address=address,
+        addresses=addresses,
         port=port,
         properties={
             "Name": atv_name.encode("utf-8"),
@@ -40,13 +49,13 @@ def mrp_service(
 def airplay_service(
     atv_name: str,
     deviceid: str,
-    address="127.0.0.1",
+    addresses: List[str] = ["127.0.0.1"],
     port: int = 7000,
     model: Optional[str] = None,
 ) -> Tuple[str, FakeDnsService]:
     service = FakeDnsService(
         name=atv_name,
-        address=address,
+        addresses=addresses,
         port=port,
         properties={"deviceid": deviceid.encode("utf-8")},
         model=model,
@@ -58,12 +67,12 @@ def homesharing_service(
     service_name: str,
     atv_name: str,
     hsgid: str,
-    address="127.0.0.1",
+    addresses: List[str] = ["127.0.0.1"],
     model: Optional[str] = None,
 ) -> Tuple[str, FakeDnsService]:
     service = FakeDnsService(
         name=service_name,
-        address=address,
+        addresses=addresses,
         port=3689,
         properties={"hG": hsgid.encode("utf-8"), "Name": atv_name.encode("utf-8")},
         model=model,
@@ -71,10 +80,15 @@ def homesharing_service(
     return ("_appletv-v2._tcp.local", service)
 
 
-def device_service(service_name, atv_name, address="127.0.0.1", model=None):
+def device_service(
+    service_name: str,
+    atv_name: str,
+    addresses: List[str] = ["127.0.0.1"],
+    model: Optional[str] = None,
+):
     service = FakeDnsService(
         name=service_name,
-        address=address,
+        addresses=addresses,
         port=3689,
         properties={"CtlN": atv_name.encode("utf-8")},
         model=model,
@@ -84,13 +98,13 @@ def device_service(service_name, atv_name, address="127.0.0.1", model=None):
 
 def companion_service(
     service_name: str,
-    address: str = "127.0.0.1",
+    addresses: List[str] = ["127.0.0.1"],
     port: int = 0,
     model: Optional[str] = None,
 ) -> Tuple[str, FakeDnsService]:
     service = FakeDnsService(
         name=service_name,
-        address=address,
+        addresses=addresses,
         port=port,
         properties={"rpHA": "33efedd528a".encode("utf-8")},
         model=model,
@@ -101,13 +115,13 @@ def companion_service(
 def raop_service(
     name: str,
     identifier: str,
-    address: str = "127.0.0.1",
+    addresses: List[str] = ["127.0.0.1"],
     port: int = 0,
     model: Optional[str] = None,
 ) -> Tuple[str, FakeDnsService]:
     service = FakeDnsService(
         name=identifier + "@" + name,
-        address=address,
+        addresses=addresses,
         port=port,
         properties={},
         model=model,
@@ -149,7 +163,7 @@ def create_response(
 
     for question in resp.questions:
         service, full_name = _lookup_service(question, services)
-        if service is None or (ip_filter and service.address != ip_filter):
+        if service is None or (ip_filter and ip_filter not in service.addresses):
             continue
 
         # For typing purposes, because service is not None, then full_name is not None
@@ -170,9 +184,9 @@ def create_response(
             rd = struct.pack(">3H", 0, 0, service.port) + local_name
             resp.resources.append(dns_utils.resource(full_name, dns.QueryType.SRV, rd))
 
-        # Add IP address
-        if service.address:
-            ipaddr = IPv4Address(service.address).packed
+        # Add IP address(es)
+        for address in service.addresses:
+            ipaddr = IPv4Address(address).packed
             resp.resources.append(
                 dns_utils.resource(service.name + ".local", dns.QueryType.A, ipaddr)
             )
@@ -242,7 +256,10 @@ def stub_multicast(udns_server, loop):
     patcher = patch("pyatv.support.mdns.multicast")
 
     async def _multicast(loop, services, **kwargs):
-        hosts = set(service.address for service in udns_server.services.values())
+        # Flatten list of lists of addresses: [["1.2.3.4"], ["2.2.2.2"]] -> ["1.2.3.4", "2.2.2.2"]
+        hosts = set(
+            chain(*[service.addresses for service in udns_server.services.values()])
+        )
         devices = {}
         sleep_proxy = udns_server.sleep_proxy
         udns_server.sleep_proxy = False
diff --git a/tests/scripts/script_env.py b/tests/scripts/script_env.py
index 2fea5d11..895f8bcd 100644
--- a/tests/scripts/script_env.py
+++ b/tests/scripts/script_env.py
@@ -55,7 +55,9 @@ class ScriptTest(AioHTTPTestCase):
         airplay_port = self.server.port
 
         self.fake_udns.add_service(
-            fake_udns.homesharing_service(DMAP_ID, "Apple TV 1", "aaaa", address=IP_1)
+            fake_udns.homesharing_service(
+                DMAP_ID, "Apple TV 1", "aaaa", addresses=[IP_1]
+            )
         )
 
         self.fake_udns.add_service(
@@ -63,13 +65,13 @@ class ScriptTest(AioHTTPTestCase):
                 "DDDD",
                 "Apple TV 2",
                 MRP_ID,
-                address=IP_2,
+                addresses=[IP_2],
                 port=self.fake_atv.get_port(Protocol.MRP),
             )
         )
         self.fake_udns.add_service(
             fake_udns.airplay_service(
-                "Apple TV 2", AIRPLAY_ID, address=IP_2, port=airplay_port
+                "Apple TV 2", AIRPLAY_ID, addresses=[IP_2], port=airplay_port
             )
         )
 
diff --git a/tests/support/dns_utils.py b/tests/support/dns_utils.py
index 76027d0f..2466a5e4 100644
--- a/tests/support/dns_utils.py
+++ b/tests/support/dns_utils.py
@@ -41,14 +41,14 @@ def add_service(
     message: mdns.DnsMessage,
     service_type: Optional[str],
     service_name: Optional[str],
-    address: Optional[str],
+    addresses: List[str],
     port: int,
     properties: Dict[str, bytes],
 ) -> None:
     if service_name is None:
         return message
 
-    if address:
+    for address in addresses:
         message.resources.append(
             resource(service_name + ".local", mdns.QueryType.A, address)
         )
@@ -88,12 +88,14 @@ def assert_service(
     message: mdns.Service,
     service_type: str,
     service_name: str,
-    address: str,
+    addresses: List[str],
     port: int,
     known_properties: Dict[str, bytes],
 ) -> None:
     assert message.type == service_type
     assert message.name == service_name
-    assert message.address == (IPv4Address(address) if address else None)
+
+    # Assume first address (if multiple) is expected
+    assert message.address == (IPv4Address(addresses[0]) if addresses else None)
     assert message.port == port
     assert message.properties == known_properties
diff --git a/tests/support/test_mdns.py b/tests/support/test_mdns.py
index 40f73583..9ed9d15c 100644
--- a/tests/support/test_mdns.py
+++ b/tests/support/test_mdns.py
@@ -21,7 +21,7 @@ DEVICE_INFO_SERVICE = "_device-info._tcp._local"
 TEST_SERVICES = dict(
     [
         fake_udns.mrp_service(
-            SERVICE_NAME, SERVICE_NAME, "mrp_id", address="127.0.0.1", port=1234
+            SERVICE_NAME, SERVICE_NAME, "mrp_id", addresses=["127.0.0.1"], port=1234
         ),
     ]
 )
@@ -131,7 +131,7 @@ def test_parse_empty_service():
 
 
 def test_parse_no_service_type():
-    service_params = (None, "service", None, 0, {})
+    service_params = (None, "service", [], 0, {})
     message = dns_utils.add_service(dns.DnsMessage(), *service_params)
 
     parsed = mdns.parse_services(message)
@@ -139,7 +139,7 @@ def test_parse_no_service_type():
 
 
 def test_parse_no_service_name():
-    service_params = ("_abc._tcp.local", None, None, 0, {})
+    service_params = ("_abc._tcp.local", None, [], 0, {})
     message = dns_utils.add_service(dns.DnsMessage(), *service_params)
 
     parsed = mdns.parse_services(message)
@@ -147,7 +147,7 @@ def test_parse_no_service_name():
 
 
 def test_parse_with_name_and_type():
-    service_params = ("_abc._tcp.local", "service", None, 0, {})
+    service_params = ("_abc._tcp.local", "service", [], 0, {})
     message = dns_utils.add_service(dns.DnsMessage(), *service_params)
 
     parsed = mdns.parse_services(message)
@@ -156,7 +156,7 @@ def test_parse_with_name_and_type():
 
 
 def test_parse_with_port_and_address():
-    service_params = ("_abc._tcp.local", "service", "10.0.0.1", 123, {})
+    service_params = ("_abc._tcp.local", "service", ["10.0.0.1"], 123, {})
     message = dns_utils.add_service(dns.DnsMessage(), *service_params)
 
     parsed = mdns.parse_services(message)
@@ -165,7 +165,7 @@ def test_parse_with_port_and_address():
 
 
 def test_parse_single_service():
-    service_params = ("_abc._tcp.local", "service", "10.0.10.1", 123, {"foo": "bar"})
+    service_params = ("_abc._tcp.local", "service", ["10.0.10.1"], 123, {"foo": "bar"})
     message = dns_utils.add_service(dns.DnsMessage(), *service_params)
 
     parsed = mdns.parse_services(message)
@@ -174,11 +174,17 @@ def test_parse_single_service():
 
 
 def test_parse_double_service():
-    service1_params = ("_abc._tcp.local", "service1", "10.0.10.1", 123, {"foo": "bar"})
+    service1_params = (
+        "_abc._tcp.local",
+        "service1",
+        ["10.0.10.1"],
+        123,
+        {"foo": "bar"},
+    )
     service2_params = (
         "_def._tcp.local",
         "service2",
-        "10.0.10.2",
+        ["10.0.10.2"],
         456,
         {"fizz": "buzz"},
     )
@@ -189,3 +195,34 @@ def test_parse_double_service():
     assert len(parsed) == 2
     dns_utils.assert_service(parsed[0], *service1_params)
     dns_utils.assert_service(parsed[1], *service2_params)
+
+
+def test_parse_pick_one_available_address():
+    addresses = ["10.0.10.1", "10.0.10.2"]
+    service_params = (
+        "_abc._tcp.local",
+        "service",
+        addresses,
+        123,
+        {"foo": "bar"},
+    )
+    message = dns_utils.add_service(dns.DnsMessage(), *service_params)
+
+    parsed = mdns.parse_services(message)
+    assert len(parsed) == 1
+    assert str(parsed[0].address) in addresses
+
+
+def test_parse_ignore_link_local_address():
+    service_params = (
+        "_abc._tcp.local",
+        "service",
+        ["169.254.1.1"],
+        123,
+        {"foo": "bar"},
+    )
+    message = dns_utils.add_service(dns.DnsMessage(), *service_params)
+
+    parsed = mdns.parse_services(message)
+    assert len(parsed) == 1
+    assert parsed[0].address is None
diff --git a/tests/support/test_mdns_functional.py b/tests/support/test_mdns_functional.py
index 5249c609..bd503398 100644
--- a/tests/support/test_mdns_functional.py
+++ b/tests/support/test_mdns_functional.py
@@ -20,7 +20,7 @@ DEVICE_INFO_SERVICE = "_device-info._tcp._local"
 TEST_SERVICES = dict(
     [
         fake_udns.mrp_service(
-            SERVICE_NAME, SERVICE_NAME, "mrp_id", address="127.0.0.1", port=1234
+            SERVICE_NAME, SERVICE_NAME, "mrp_id", addresses=["127.0.0.1"], port=1234
         ),
     ]
 )
@@ -201,7 +201,7 @@ async def test_multicast_sleeping_device(event_loop, udns_server, multicast_fast
 
     udns_server.services = {
         MEDIAREMOTE_SERVICE: fake_udns.FakeDnsService(
-            name=SERVICE_NAME, address=None, port=0, properties={}, model=None
+            name=SERVICE_NAME, addresses=[], port=0, properties={}, model=None
         ),
     }
 
@@ -252,7 +252,7 @@ async def test_multicast_device_model(event_loop, udns_server, multicast_fastexi
     udns_server.services = {
         MEDIAREMOTE_SERVICE: fake_udns.FakeDnsService(
             name=SERVICE_NAME,
-            address="127.0.0.1",
+            addresses=["127.0.0.1"],
             port=1234,
             properties={},
             model="dummy",
diff --git a/tests/test_scan_functional.py b/tests/test_scan_functional.py
index a46b1a98..8f28af79 100644
--- a/tests/test_scan_functional.py
+++ b/tests/test_scan_functional.py
@@ -91,17 +91,17 @@ async def test_multicast_scan(
     multicast_scan: Scanner,
 ):
     udns_server.add_service(
-        fake_udns.mrp_service("Apple TV", "Apple TV MRP", MRP_ID_1, address=IP_1)
+        fake_udns.mrp_service("Apple TV", "Apple TV MRP", MRP_ID_1, addresses=[IP_1])
     )
     udns_server.add_service(
-        fake_udns.homesharing_service("abcd", "Apple TV HS 2", HSGID, address=IP_2)
+        fake_udns.homesharing_service("abcd", "Apple TV HS 2", HSGID, addresses=[IP_2])
     )
     udns_server.add_service(
-        fake_udns.device_service("efgh", "Apple TV Device", address=IP_3)
+        fake_udns.device_service("efgh", "Apple TV Device", addresses=[IP_3])
     )
     udns_server.add_service(
         fake_udns.companion_service(
-            "Companion Device", address=IP_3, port=COMPANION_PORT
+            "Companion Device", addresses=[IP_3], port=COMPANION_PORT
         )
     )
 
@@ -127,7 +127,7 @@ async def test_multicast_scan(
 @pytest.mark.asyncio
 async def test_multicast_scan_no_home_sharing(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.device_service("efgh", "Apple TV Device", address=IP_3)
+        fake_udns.device_service("efgh", "Apple TV Device", addresses=[IP_3])
     )
 
     atvs = await multicast_scan()
@@ -141,10 +141,12 @@ async def test_multicast_scan_no_home_sharing(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_home_sharing_merge(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.device_service("efgh", "Apple TV Device", address=IP_3)
+        fake_udns.device_service("efgh", "Apple TV Device", addresses=[IP_3])
     )
     udns_server.add_service(
-        fake_udns.homesharing_service("efgh", "Apple TV Device", HSGID, address=IP_3)
+        fake_udns.homesharing_service(
+            "efgh", "Apple TV Device", HSGID, addresses=[IP_3]
+        )
     )
 
     atvs = await multicast_scan()
@@ -158,10 +160,12 @@ async def test_multicast_scan_home_sharing_merge(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_mrp(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.mrp_service("Apple TV 1", "Apple TV MRP 1", MRP_ID_1, address=IP_1)
+        fake_udns.mrp_service(
+            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, addresses=[IP_1]
+        )
     )
     udns_server.add_service(
-        fake_udns.device_service("efgh", "Apple TV Device", address=IP_3)
+        fake_udns.device_service("efgh", "Apple TV Device", addresses=[IP_3])
     )
 
     atvs = await multicast_scan(protocol=Protocol.MRP)
@@ -176,10 +180,12 @@ async def test_multicast_scan_mrp(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_mrp_with_companion(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.mrp_service("Apple TV 1", "Apple TV MRP 1", MRP_ID_1, address=IP_1)
+        fake_udns.mrp_service(
+            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, addresses=[IP_1]
+        )
     )
     udns_server.add_service(
-        fake_udns.companion_service("Companion", IP_1, COMPANION_PORT)
+        fake_udns.companion_service("Companion", addresses=[IP_1], port=COMPANION_PORT)
     )
 
     atvs = await multicast_scan(protocol=Protocol.MRP)
@@ -198,7 +204,7 @@ async def test_multicast_scan_mrp_with_companion(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_companion_device(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.companion_service("Companion", IP_1, COMPANION_PORT)
+        fake_udns.companion_service("Companion", addresses=[IP_1], port=COMPANION_PORT)
     )
 
     atvs = await multicast_scan()
@@ -208,7 +214,7 @@ async def test_multicast_scan_companion_device(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_airplay_device(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.airplay_service("Apple TV", AIRPLAY_ID, address=IP_1)
+        fake_udns.airplay_service("Apple TV", AIRPLAY_ID, addresses=[IP_1])
     )
 
     atvs = await multicast_scan()
@@ -221,7 +227,7 @@ async def test_multicast_scan_airplay_device(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_raop_device(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.raop_service("Apple TV", RAOP_ID, IP_1, RAOP_PORT)
+        fake_udns.raop_service("Apple TV", RAOP_ID, addresses=[IP_1], port=RAOP_PORT)
     )
 
     atvs = await multicast_scan()
@@ -234,10 +240,14 @@ async def test_multicast_scan_raop_device(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_for_particular_device(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.mrp_service("Apple TV 1", "Apple TV MRP 1", MRP_ID_1, address=IP_1)
+        fake_udns.mrp_service(
+            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, addresses=[IP_1]
+        )
     )
     udns_server.add_service(
-        fake_udns.homesharing_service("efgh", "Apple TV Device", HSGID, address=IP_3)
+        fake_udns.homesharing_service(
+            "efgh", "Apple TV Device", HSGID, addresses=[IP_3]
+        )
     )
 
     atvs = await multicast_scan(identifier="efgh")
@@ -251,7 +261,9 @@ async def test_multicast_scan_deep_sleeping_device(udns_server, multicast_scan):
     udns_server.sleep_proxy = True
 
     udns_server.add_service(
-        fake_udns.mrp_service("Apple TV 1", "Apple TV MRP 1", MRP_ID_1, address=IP_1)
+        fake_udns.mrp_service(
+            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, addresses=[IP_1]
+        )
     )
 
     atvs = await multicast_scan()
@@ -262,10 +274,12 @@ async def test_multicast_scan_deep_sleeping_device(udns_server, multicast_scan):
 @pytest.mark.asyncio
 async def test_multicast_scan_device_info(udns_server, multicast_scan):
     udns_server.add_service(
-        fake_udns.mrp_service("Apple TV 1", "Apple TV MRP 1", MRP_ID_1, address=IP_1)
+        fake_udns.mrp_service(
+            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, addresses=[IP_1]
+        )
     )
     udns_server.add_service(
-        fake_udns.airplay_service("Apple TV", AIRPLAY_ID, address=IP_1)
+        fake_udns.airplay_service("Apple TV", AIRPLAY_ID, addresses=[IP_1])
     )
 
     atvs = await multicast_scan(protocol=Protocol.MRP)
@@ -279,7 +293,7 @@ async def test_multicast_scan_device_info(udns_server, multicast_scan):
 async def test_multicast_scan_device_model(udns_server, multicast_scan):
     udns_server.add_service(
         fake_udns.mrp_service(
-            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, address=IP_1, model="J105aAP"
+            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, addresses=[IP_1], model="J105aAP"
         )
     )
 
@@ -328,7 +342,7 @@ async def test_unicast_scan_mrp(udns_server, unicast_scan):
 async def test_unicast_scan_airplay(udns_server, unicast_scan):
     udns_server.add_service(
         fake_udns.airplay_service(
-            "Apple TV AirPlay", AIRPLAY_ID, address=IP_1, port=7000
+            "Apple TV AirPlay", AIRPLAY_ID, addresses=[IP_1], port=7000
         )
     )
 
@@ -348,7 +362,7 @@ async def test_unicast_scan_airplay(udns_server, unicast_scan):
 @pytest.mark.asyncio
 async def test_unicast_scan_raop(udns_server, unicast_scan):
     udns_server.add_service(
-        fake_udns.raop_service("Apple TV", RAOP_ID, address=IP_1, port=RAOP_PORT)
+        fake_udns.raop_service("Apple TV", RAOP_ID, addresses=[IP_1], port=RAOP_PORT)
     )
 
     atvs = await unicast_scan()
@@ -372,7 +386,7 @@ async def test_unicast_scan_comapnion(udns_server, unicast_scan):
 @pytest.mark.asyncio
 async def test_unicast_scan_homesharing(udns_server, unicast_scan):
     udns_server.add_service(
-        fake_udns.homesharing_service("abcd", "Apple TV HS", HSGID, address=IP_3)
+        fake_udns.homesharing_service("abcd", "Apple TV HS", HSGID, addresses=[IP_3])
     )
 
     atvs = await unicast_scan()
@@ -392,7 +406,7 @@ async def test_unicast_scan_homesharing(udns_server, unicast_scan):
 @pytest.mark.asyncio
 async def test_unicast_scan_no_homesharing(udns_server, unicast_scan):
     udns_server.add_service(
-        fake_udns.device_service("Apple TV", "Apple TV Device", address=IP_2)
+        fake_udns.device_service("Apple TV", "Apple TV Device", addresses=[IP_2])
     )
 
     atvs = await unicast_scan()
@@ -411,10 +425,10 @@ async def test_unicast_scan_no_homesharing(udns_server, unicast_scan):
 @pytest.mark.asyncio
 async def test_unicast_scan_device_info(udns_server, unicast_scan):
     udns_server.add_service(
-        fake_udns.mrp_service("Apple TV", "Apple TV MRP", MRP_ID_1, address=IP_1)
+        fake_udns.mrp_service("Apple TV", "Apple TV MRP", MRP_ID_1, addresses=[IP_1])
     )
     udns_server.add_service(
-        fake_udns.airplay_service("Apple TV", AIRPLAY_ID, address=IP_1)
+        fake_udns.airplay_service("Apple TV", AIRPLAY_ID, addresses=[IP_1])
     )
 
     atvs = await unicast_scan()
@@ -428,7 +442,7 @@ async def test_unicast_scan_device_info(udns_server, unicast_scan):
 async def test_unicast_scan_device_model(udns_server, unicast_scan):
     udns_server.add_service(
         fake_udns.mrp_service(
-            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, address=IP_1, model="J105aAP"
+            "Apple TV 1", "Apple TV MRP 1", MRP_ID_1, addresses=[IP_1], model="J105aAP"
         )
     )