From 7def5c4464471e9868504d1e6ad43a25a4c00fde Mon Sep 17 00:00:00 2001 From: Nose Date: Wed, 14 Jan 2026 14:32:46 -0800 Subject: [PATCH] m --- API/zerotier.py | 64 ++++++++++++++--- TUI/modalscreen/config_modal.py | 119 ++++++++++++-------------------- 2 files changed, 101 insertions(+), 82 deletions(-) diff --git a/API/zerotier.py b/API/zerotier.py index fab3724..9505bec 100644 --- a/API/zerotier.py +++ b/API/zerotier.py @@ -404,6 +404,27 @@ def get_assigned_addresses(network_id: str) -> List[str]: return [] +def get_assigned_subnets(network_id: str) -> List[str]: + """Return CIDR subnets (e.g. '10.147.17.0/24') for the given network.""" + network_id = str(network_id or "").strip() + if not network_id: + return [] + + subnets = [] + for n in list_networks(): + if n.id == network_id: + for addr in n.assigned_addresses: + if addr and "/" in addr: + # Calculate subnet base + try: + import ipaddress + net = ipaddress.ip_network(addr, strict=False) + subnets.append(str(net)) + except Exception: + pass + return subnets + + def fetch_central_members(network_id: str, api_token: str) -> List[Dict[str, Any]]: """Fetch member details from ZeroTier Central API. @@ -518,17 +539,30 @@ def discover_services_on_network( addr = str(ip).split("/")[0] if addr not in addresses: addresses.append(addr) + else: + # Fallback: if no Central token, and we are on a likely /24 subnet, + # we can try to guess/probe peers on that same subnet. + subnets = get_assigned_subnets(net) + for subnet_str in subnets: + try: + import ipaddress + subnet = ipaddress.ip_network(subnet_str, strict=False) + # Only scan if subnet is reasonably small (e.g. <= /24 = 256 hosts) + if subnet.num_addresses <= 256: + for ip in subnet.hosts(): + addr = str(ip) + if addr not in addresses: + addresses.append(addr) + except Exception: + pass probes: List[ZeroTierServiceProbe] = [] - for addr in addresses: - host = str(addr or "").strip() - if not host: - continue - - # Performance optimization: if we have many addresses, skip those clearly not on our ZT subnet - # (Though fetch_central_members already filters for this network) - + # Parallelize probes to make subnet scanning feasible + import concurrent.futures + + def do_probe(host): + host_probes = [] for port in ports: # Try HTTP first as it's the common case for local storage for scheme in ("http", "https"): @@ -550,7 +584,7 @@ def discover_services_on_network( except Exception: pass - probes.append(ZeroTierServiceProbe( + host_probes.append(ZeroTierServiceProbe( address=host, port=int(port), path=path, @@ -562,6 +596,18 @@ def discover_services_on_network( )) # Stop probing other schemes/paths for this host/port break + return host_probes + + # Use ThreadPoolExecutor for concurrent I/O probes + max_workers = min(50, len(addresses) or 1) + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: + future_to_addr = {executor.submit(do_probe, addr): addr for addr in addresses} + for future in concurrent.futures.as_completed(future_to_addr): + try: + probes.extend(future.result()) + except Exception: + pass + return probes diff --git a/TUI/modalscreen/config_modal.py b/TUI/modalscreen/config_modal.py index 2669d11..895b9e4 100644 --- a/TUI/modalscreen/config_modal.py +++ b/TUI/modalscreen/config_modal.py @@ -736,7 +736,7 @@ class ConfigModal(ModalScreen): # 1. Choose Network joined = zt.list_networks() if not joined: - self.notify("Error: Join a ZeroTier network first in 'Networking'", severity="error") + self.notify("Error: Join a ZeroTier network first in 'Connectors'", severity="error") return net_options = [f"{n.name or 'Network'} ({n.id})" for n in joined] @@ -786,87 +786,60 @@ class ConfigModal(ModalScreen): self.app.push_screen(SelectionModal("Select Local Store to Share", local_stores), callback=on_share_selected) else: - # 3b. Connect to Remote Peer - # Discover Peers (Port 999 only) - central_token = self.config_data.get("networking", {}).get("zerotier", {}).get("api_key") - self.notify(f"Scanning Network {net_id} for peers...") - - try: - probes = zt.discover_services_on_network(net_id, ports=[999], api_token=central_token) - except Exception as e: - self.notify(f"Discovery error: {e}", severity="error") - return + # 3b. Connect to Remote Peer - Background Discovery + @work + async def run_discovery(): + self.notify(f"Discovery: Scanning {net_id} for peers...", timeout=5) + central_token = self.config_data.get("networking", {}).get("zerotier", {}).get("api_key") + try: + import asyncio + from functools import partial + loop = asyncio.get_event_loop() + probes = await loop.run_in_executor(None, partial( + zt.discover_services_on_network, net_id, ports=[999, 45869], api_token=central_token + )) + except Exception as e: + self.notify(f"Discovery error: {e}", severity="error") + return - if not probes: - self.notify("No peers found on port 999. Use manual setup.", severity="warning") - # Create empty template - new_name = f"zt_remote" + if not probes: + self.notify("No peers found. Check firewall or server status.", severity="warning") + return + + peer_options = [] + for p in probes: + label = "Remote" + if isinstance(p.payload, dict): + label = p.payload.get("name") or p.payload.get("peer_id") or label + status = " [Locked]" if p.status_code == 401 else "" + peer_options.append(f"{p.address} ({label}){status}") + + def on_peer_selected(choice: str): + if not choice: return + addr = choice.split(" ")[0] + match = next((p for p in probes if p.address == addr), None) + if match: + save_connected_store(match) + + self.app.push_screen(SelectionModal("Select Peer to Connect", peer_options), callback=on_peer_selected) + + def save_connected_store(p: zt.ZeroTierServiceProbe): + new_name = f"zt_{p.address.replace('.', '_')}" if "store" not in self.config_data: self.config_data["store"] = {} store_cfg = self.config_data["store"].setdefault("zerotier", {}) + store_cfg[new_name] = { "NAME": new_name, "NETWORK_ID": net_id, - "HOST": "", - "PORT": "999", - "SERVICE": "remote" + "HOST": p.address, + "PORT": str(p.port), + "SERVICE": p.service_hint or "remote" } - try: - self.save_all() - self.notify("ZeroTier manual template created.") - except Exception as e: - self.notify(f"Auto-save failed: {e}", severity="error") - - self.editing_item_type = "store-zerotier" - self.editing_item_name = new_name + self.save_all() + self.notify(f"Connected to {p.address}") self.refresh_view() - return - peer_options = [] - for p in probes: - peer_name = "Unnamed Peer" - if isinstance(p.payload, dict): - peer_name = p.payload.get("name") or p.payload.get("NAME") or peer_name - - status_label = "" - if p.status_code == 401: - status_label = " [Locked/401]" - - peer_options.append(f"{p.address} ({peer_name}){status_label}") - - def on_peer_selected(peer_choice: str): - if not peer_choice: return - p_addr = peer_choice.split(" ")[0] - match = next((p for p in probes if p.address == p_addr), None) - - new_name = f"zt_{p_addr.replace('.', '_')}" - if "store" not in self.config_data: self.config_data["store"] = {} - store_cfg = self.config_data["store"].setdefault("zerotier", {}) - - new_config = { - "NAME": new_name, - "NETWORK_ID": net_id, - "HOST": p_addr, - "PORT": "999", - "SERVICE": "remote" - } - if match: - if match.service_hint == "hydrus": - new_config["SERVICE"] = "hydrus" - new_config["PORT"] = "45869" - if match.status_code == 401: - self.notify("This peer requires an API Key. Please enter it in the settings panel.", severity="warning") - - store_cfg[new_name] = new_config - - try: - self.save_all() - self.notify(f"ZeroTier auto-saved: Store '{new_name}' added.") - except Exception as e: - self.notify(f"Auto-save failed: {e}", severity="error") - - self.editing_item_type = "store-zerotier" - self.editing_item_name = new_name - self.refresh_view() + run_discovery() self.app.push_screen(SelectionModal("Select Remote Peer", peer_options), callback=on_peer_selected)