Removed netifaces for better compatibility

This commit is contained in:
Abolfazl
2026-04-23 13:29:50 +03:30
parent 57738ec5c8
commit fb75ba4ea9
4 changed files with 91 additions and 77 deletions
+1 -1
View File
@@ -278,7 +278,7 @@ Install everything from [`requirements.txt`](requirements.txt). All listed packa
| `h2` | HTTP/2 multiplexing to the Apps Script relay (significantly faster) |
| `brotli` | Decompression of `Content-Encoding: br` responses |
| `zstandard` | Decompression of `Content-Encoding: zstd` responses |
| `netifaces` | Better network interface detection for LAN sharing (fallback available without it) |
### Load Balancing
-1
View File
@@ -226,7 +226,6 @@ json
| `h2` | ارتباط HTTP/2 با رله Apps Script (به‌طور محسوسی سریع‌تر) |
| `brotli` | پشتیبانی از فشرده‌سازی `Content-Encoding: br` |
| `zstandard` | پشتیبانی از فشرده‌سازی `Content-Encoding: zstd` |
| `netifaces` | تشخیص بهتر اینترفیس‌های شبکه برای اشتراک‌گذاری LAN (در صورت نبود آن، حالت جایگزین در دسترس است) |
### استفاده از چند Script ID
+2 -2
View File
@@ -13,5 +13,5 @@ brotli>=1.1.0
# Optional: Zstandard decompression (some CDNs now serve `zstd`)
zstandard>=0.22.0
# Optional: Better network interface detection for LAN sharing
netifaces>=0.11.0
# LAN interface detection now uses only the Python standard library
# (works on Windows, Linux, macOS, Android/Termux without a C compiler).
+88 -73
View File
@@ -3,92 +3,104 @@ LAN utilities for detecting network interfaces and IP addresses.
Provides functionality to enumerate local network interfaces and their
associated IP addresses for LAN proxy sharing.
Implementation notes
--------------------
This module intentionally relies only on the Python standard library so
that it works out-of-the-box on every supported OS (Windows, Linux,
macOS, Android/Termux, *BSD) without requiring a C compiler or native
build tools (previous versions depended on ``netifaces``, which needs
"Microsoft Visual C++ 14.0 or greater" on Windows and was a frequent
install blocker for users on slow connections).
Strategy (in order):
1. "UDP connect trick" to reliably discover the primary outbound
IPv4 and IPv6 addresses on any OS.
2. ``socket.getaddrinfo(hostname, ...)`` to enumerate additional
addresses bound to the host (covers multi-homed machines).
These two steps together cover every real-world LAN scenario on
Windows, Linux, macOS, Android/Termux and *BSD. (We intentionally do
*not* try to map per-interface names to IPs via the stdlib — that is
not portable and, on Windows, triggers 30 s DNS timeouts.)
"""
import ipaddress
import logging
import socket
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Set
log = logging.getLogger("LAN")
# ---------------------------------------------------------------------------
# Primary-IP discovery (UDP connect trick)
# ---------------------------------------------------------------------------
def _primary_ip(family: int, probe_addr: str) -> Optional[str]:
"""
Return the primary local IP the OS would use to reach ``probe_addr``.
Uses a connected UDP socket which does *not* actually send packets —
the kernel just picks the source address from its routing table.
Works identically on Windows, Linux, macOS, and Android.
"""
s = socket.socket(family, socket.SOCK_DGRAM)
try:
s.settimeout(0.5)
# Port 80 is arbitrary; no packet is sent for UDP connect().
s.connect((probe_addr, 80))
ip = s.getsockname()[0]
if family == socket.AF_INET6:
ip = ip.split('%', 1)[0] # strip zone id
return ip
except OSError:
return None
finally:
s.close()
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def get_network_interfaces() -> Dict[str, List[str]]:
"""
Get all network interfaces and their associated IP addresses.
Returns a dictionary mapping interface names to lists of IP addresses
(both IPv4 and IPv6). Only includes interfaces with valid IP addresses
that are not loopback.
Get network interfaces and their associated non-loopback IP addresses.
Returns:
Dict[str, List[str]]: Interface name -> list of IP addresses
Dict[str, List[str]]: Interface label -> list of IP addresses.
Interface names are best-effort; on some platforms we fall back
to synthetic labels such as ``"primary"`` / ``"primary_ipv6"``.
"""
interfaces = {}
interfaces: Dict[str, List[str]] = {}
seen_ips: Set[str] = set()
def _add(label: str, ip: Optional[str]) -> None:
if not ip or ip in seen_ips:
return
if ip.startswith('127.') or ip == '::1':
return
seen_ips.add(ip)
interfaces.setdefault(label, []).append(ip)
# 1) Primary outbound IPs (most reliable, cross-platform).
_add('primary', _primary_ip(socket.AF_INET, '192.0.2.1')) # TEST-NET-1
_add('primary_ipv6', _primary_ip(socket.AF_INET6, '2001:db8::1')) # doc prefix
# 2) Enumerate via hostname resolution (picks up multi-homed hosts).
try:
import netifaces
for iface in netifaces.interfaces():
addrs = netifaces.ifaddresses(iface)
ips = []
# IPv4 addresses
if netifaces.AF_INET in addrs:
for addr in addrs[netifaces.AF_INET]:
ip = addr.get('addr')
if ip and not ip.startswith('127.'):
ips.append(ip)
# IPv6 addresses (without scope)
if netifaces.AF_INET6 in addrs:
for addr in addrs[netifaces.AF_INET6]:
ip = addr.get('addr')
if ip and not ip.startswith('::1') and not '%' in ip:
# Remove scope if present
ips.append(ip.split('%')[0])
if ips:
interfaces[iface] = ips
except ImportError:
# Fallback to socket method for basic detection
log.debug("netifaces not available, using socket fallback")
interfaces = _get_interfaces_socket_fallback()
return interfaces
def _get_interfaces_socket_fallback() -> Dict[str, List[str]]:
"""
Fallback method to get network interfaces using socket.
This is less comprehensive than netifaces but works without extra dependencies.
"""
interfaces = {}
try:
# Get hostname and try to resolve to IPs
hostname = socket.gethostname()
try:
# Get IPv4 addresses
ipv4_info = socket.getaddrinfo(hostname, None, socket.AF_INET)
ipv4_addrs = [info[4][0] for info in ipv4_info if not info[4][0].startswith('127.')]
if ipv4_addrs:
interfaces['primary'] = list(set(ipv4_addrs)) # Remove duplicates
except socket.gaierror:
pass
except OSError:
hostname = ''
for family, label in ((socket.AF_INET, 'host'), (socket.AF_INET6, 'host_ipv6')):
try:
# Get IPv6 addresses
ipv6_info = socket.getaddrinfo(hostname, None, socket.AF_INET6)
ipv6_addrs = []
for info in ipv6_info:
for info in socket.getaddrinfo(hostname, None, family):
ip = info[4][0]
if not ip.startswith('::1') and not '%' in ip:
ipv6_addrs.append(ip.split('%')[0])
if ipv6_addrs:
interfaces['primary_ipv6'] = list(set(ipv6_addrs))
except socket.gaierror:
pass
except Exception as e:
log.debug("Socket fallback failed: %s", e)
if family == socket.AF_INET6:
ip = ip.split('%', 1)[0]
_add(label, ip)
except (socket.gaierror, OSError):
continue
return interfaces
@@ -107,21 +119,24 @@ def get_lan_ips(port: int = 8085) -> List[str]:
List[str]: List of "IP:port" strings for LAN access
"""
interfaces = get_network_interfaces()
lan_addresses = []
lan_addresses: List[str] = []
for iface_ips in interfaces.values():
for ip in iface_ips:
try:
# Validate IP and check if it's a private address
addr = ipaddress.ip_address(ip)
if addr.is_private or addr.is_link_local:
lan_addresses.append(f"{ip}:{port}")
except ValueError:
continue
if addr.is_loopback or addr.is_unspecified:
continue
# Include private, link-local, and unique-local (IPv6 fc00::/7) ranges.
if addr.is_private or addr.is_link_local:
bracket = f"[{ip}]" if isinstance(addr, ipaddress.IPv6Address) else ip
lan_addresses.append(f"{bracket}:{port}")
# Remove duplicates while preserving order
seen = set()
unique_addresses = []
# Remove duplicates while preserving order.
seen: Set[str] = set()
unique_addresses: List[str] = []
for addr in lan_addresses:
if addr not in seen:
seen.add(addr)