Преглед на файлове

fix(network-utils): enumerate interfaces on Windows via psutil

  get_network_interfaces() used fcntl ioctls and get_all_interface_ips()
  shelled out to `ip -j addr show` — both Linux-only. On Windows the
  fcntl path raised ImportError and returned [], so the VP "bind IP"
  dropdown showed no interfaces.

  Add a Windows branch using psutil.net_if_addrs() + net_if_stats()
  (psutil is already a dep). Same dict shape as the Linux path, filters
  loopback / link-local / down interfaces by address class instead of
  by name prefix. No name-based exclusion — users may legitimately
  bind a VP to a Hyper-V / WSL / Tailscale virtual adapter.

  The Linux fcntl path is untouched.
maziggy преди 2 месеца
родител
ревизия
4b2babe752
променени са 1 файла, в които са добавени 79 реда и са изтрити 1 реда
  1. 79 1
      backend/app/services/network_utils.py

+ 79 - 1
backend/app/services/network_utils.py

@@ -7,10 +7,14 @@ import shutil
 import socket
 import socket
 import struct
 import struct
 import subprocess
 import subprocess
+import sys
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-# Interfaces to exclude from selection
+# Interfaces to exclude from selection (Linux only — Windows adapter names
+# don't follow these prefixes and there's no equivalent uniform Windows
+# exclude list worth hard-coding; the psutil path filters on address class
+# (loopback, link-local) and interface up-state instead).
 EXCLUDED_INTERFACE_PREFIXES = ("lo", "docker", "br-", "veth", "virbr")
 EXCLUDED_INTERFACE_PREFIXES = ("lo", "docker", "br-", "veth", "virbr")
 
 
 # Resolve full path to `ip` command (may not be in PATH for service users)
 # Resolve full path to `ip` command (may not be in PATH for service users)
@@ -22,12 +26,86 @@ def _is_excluded(name: str) -> bool:
     return any(name.startswith(prefix) for prefix in EXCLUDED_INTERFACE_PREFIXES)
     return any(name.startswith(prefix) for prefix in EXCLUDED_INTERFACE_PREFIXES)
 
 
 
 
+def _get_network_interfaces_psutil() -> list[dict]:
+    """Windows path: enumerate interfaces via psutil.
+
+    fcntl + ioctl is Linux-only, and the ``ip`` command isn't available
+    on Windows either, so both Linux code paths return empty here. psutil
+    is already a Bambuddy dep (``psutil>=6.0.0``) and gives us cross-
+    platform name + IPv4 + netmask in one call.
+
+    Filters: IPv4 only (matches the Linux path), skip loopback and
+    link-local (169.254.0.0/16), skip interfaces psutil reports as down.
+    No name-based exclusion — users on Windows may legitimately want to
+    bind a VP to a Hyper-V / WSL / Tailscale virtual adapter.
+    """
+    try:
+        import psutil
+    except ImportError:
+        logger.warning("psutil not available, interface detection unavailable on this platform")
+        return []
+
+    interfaces = []
+    try:
+        addrs_by_iface = psutil.net_if_addrs()
+        stats_by_iface = psutil.net_if_stats()
+    except Exception as e:
+        logger.error("psutil failed to enumerate interfaces: %s", e)
+        return []
+
+    for name, addrs in addrs_by_iface.items():
+        stats = stats_by_iface.get(name)
+        if stats is not None and not stats.isup:
+            continue
+
+        for addr in addrs:
+            if addr.family != socket.AF_INET:
+                continue
+            ip = addr.address
+            netmask = addr.netmask
+            if not ip or not netmask:
+                continue
+
+            try:
+                ip_obj = ipaddress.IPv4Address(ip)
+            except ValueError:
+                continue
+            if ip_obj.is_loopback or ip_obj.is_link_local:
+                continue
+
+            try:
+                network = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False)
+            except ValueError:
+                continue
+
+            interfaces.append(
+                {
+                    "name": name,
+                    "ip": ip,
+                    "netmask": netmask,
+                    "subnet": str(network),
+                }
+            )
+            # First IPv4 per interface is enough; matches Linux ioctl which
+            # returns only the primary IP (aliases land via get_all_interface_ips
+            # on Linux, which has no Windows analogue worth replicating).
+            break
+
+    return interfaces
+
+
 def get_network_interfaces() -> list[dict]:
 def get_network_interfaces() -> list[dict]:
     """Get all network interfaces with their IPs and subnets.
     """Get all network interfaces with their IPs and subnets.
 
 
     Returns:
     Returns:
         List of dicts with name, ip, netmask, subnet, broadcast
         List of dicts with name, ip, netmask, subnet, broadcast
     """
     """
+    # Windows has no fcntl and no `ip` binary; the Linux ioctl path below
+    # raises ImportError on import fcntl. Route to the psutil-based path
+    # instead. The Linux path stays as-is for behavioural parity.
+    if sys.platform == "win32":
+        return _get_network_interfaces_psutil()
+
     interfaces = []
     interfaces = []
 
 
     try:
     try: