diff --git a/pyatv/support/net.py b/pyatv/support/net.py index f6a9b913c..87ef09735 100644 --- a/pyatv/support/net.py +++ b/pyatv/support/net.py @@ -14,6 +14,9 @@ _LOGGER = logging.getLogger(__name__) +# Private RFC1918 target used only to select a default route. +_DEFAULT_ROUTE_PROBE = ("10.255.255.1", 1) + def unused_port() -> int: """Return a port that is unused on the current host.""" @@ -47,6 +50,26 @@ def mcast_socket(address: Optional[str], port: int = 0) -> socket.socket: with suppress(OSError): membership = socket.inet_aton("224.0.0.251") + socket.inet_aton(address) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, membership) + else: + # Fallback for sandboxed platforms where ifaddr finds no adapters: + # detect default IPv4 via a dummy UDP connect and use it for multicast join. + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe: + probe.connect(_DEFAULT_ROUTE_PROBE) + detected = probe.getsockname()[0] + sock.setsockopt( + socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(detected) + ) + membership = socket.inet_aton("224.0.0.251") + socket.inet_aton( + detected + ) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, membership) + except OSError: + with suppress(OSError): + membership = socket.inet_aton("224.0.0.251") + socket.inet_aton( + "0.0.0.0" + ) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, membership) _LOGGER.debug("Binding on %s:%d", address or "*", port) sock.bind((address or "", port)) @@ -74,6 +97,17 @@ def get_private_addresses(include_loopback=True) -> List[IPv4Address]: if ipaddr.is_private: addresses.append(ipaddr) + # Fallback when no adapters are reported (e.g. sandboxed platforms). + if not addresses: + try: + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe: + probe.connect(_DEFAULT_ROUTE_PROBE) + detected = IPv4Address(probe.getsockname()[0]) + if include_loopback or not detected.is_loopback: + addresses.append(detected) + except OSError: + pass + return addresses