m
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user