66 lines
2.2 KiB
Python
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]
|