mdns-termux/lib/mdns_tools/_pick.py

66 lines
2.2 KiB
Python

"""Pick the most reachable address when a host advertises several.
mDNS hosts with virtual bridges (libvirt, docker) or multiple NICs announce
every local IP. Many are not routable from us. Prefer an address in our own
subnet, then drop known-virtual ranges, else fall back to the first.
"""
import ipaddress
import socket
# Bridge/virtual ranges that are usually not reachable across hosts.
_AVOID = [
ipaddress.ip_network("192.168.122.0/24"), # libvirt default (virbr0)
ipaddress.ip_network("172.17.0.0/16"), # docker default (docker0)
ipaddress.ip_network("169.254.0.0/16"), # link-local autoconf
]
def local_ipv4() -> str | None:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
except OSError:
return None
finally:
s.close()
def _avoided(ip: str) -> bool:
try:
addr = ipaddress.ip_address(ip)
except ValueError:
return True
return any(addr in net for net in _AVOID)
def pick_best(candidates: list[str], family: str = "v4") -> str | None:
"""Return the best-reachable address, or None if none look routable.
Returning None on all-unreachable is deliberate: it lets the caller fall
through to the next resolution method (e.g. Tailscale) instead of handing
back a dead virtual-bridge IP.
"""
uniq: list[str] = []
for c in candidates:
if c and c not in uniq:
uniq.append(c)
if not uniq:
return None
if family == "v4":
mine = local_ipv4()
if mine:
try:
my_net = ipaddress.ip_network(mine + "/24", strict=False)
same = [c for c in uniq if ipaddress.ip_address(c) in my_net]
if same:
return same[0]
except ValueError:
pass
# Drop known virtual/bridge ranges; if nothing remains, signal None.
good = [c for c in uniq if not _avoided(c)]
return good[0] if good else None
# IPv6: prefer global/ULA over link-local (fe80::)
non_ll = [c for c in uniq if not c.lower().startswith("fe80")]
return non_ll[0] if non_ll else uniq[0]