Skip to main content
Glama
by appleton
network-discovery.ts7.22 kB
import * as net from "net"; import { execSync } from "child_process"; import * as os from "os"; interface NetworkDevice { ip: string; mac?: string; vendor?: string; ports: number[]; isLikelyRoboVac: boolean; } export class NetworkDiscovery { private readonly TUYA_PORTS = [6668, 6667, 443]; private readonly ANKER_EUFY_OUIS = [ "34:ea:34", // Anker Innovations Limited "70:55:82", // Anker Innovations Limited "90:9a:4a", // Anker Innovations Limited "a4:c1:38", // Anker Innovations Limited "2c:aa:8e", // Anker Innovations Limited ]; private getLocalNetworkRange(): string { const interfaces = os.networkInterfaces(); for (const [name, addrs] of Object.entries(interfaces)) { if (!addrs) continue; for (const addr of addrs) { if (addr.family === "IPv4" && !addr.internal) { const parts = addr.address.split("."); if (parts[0] === "192" && parts[1] === "168") { return `${parts[0]}.${parts[1]}.${parts[2]}.0/24`; } else if (parts[0] === "10") { return "10.0.0.0/24"; } else if ( parts[0] === "172" && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31 ) { return `172.${parts[1]}.0.0/16`; } } } } return "192.168.1.0/24"; // Default fallback } private async scanPort( ip: string, port: number, timeout: number = 1000 ): Promise<boolean> { return new Promise((resolve) => { const socket = new net.Socket(); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, timeout); socket.on("connect", () => { clearTimeout(timer); socket.destroy(); resolve(true); }); socket.on("error", () => { clearTimeout(timer); resolve(false); }); socket.connect(port, ip); }); } private async getArpTable(): Promise<Map<string, string>> { const arpMap = new Map<string, string>(); try { let arpOutput: string; const platform = os.platform(); if (platform === "darwin" || platform === "linux") { arpOutput = execSync("arp -a", { encoding: "utf8", timeout: 5000 }); } else if (platform === "win32") { arpOutput = execSync("arp -a", { encoding: "utf8", timeout: 5000 }); } else { return arpMap; } const lines = arpOutput.split("\n"); for (const line of lines) { // Parse different ARP formats let match; if (platform === "darwin") { // macOS format: hostname (192.168.1.100) at aa:bb:cc:dd:ee:ff [ether] on en0 match = line.match(/\((\d+\.\d+\.\d+\.\d+)\) at ([a-fA-F0-9:]{17})/); } else if (platform === "linux") { // Linux format: 192.168.1.100 ether aa:bb:cc:dd:ee:ff C eth0 match = line.match(/(\d+\.\d+\.\d+\.\d+).*?([a-fA-F0-9:]{17})/); } else if (platform === "win32") { // Windows format: 192.168.1.100 aa-bb-cc-dd-ee-ff dynamic match = line.match(/(\d+\.\d+\.\d+\.\d+)\s+([a-fA-F0-9-]{17})/); if (match) { match[2] = match[2].replace(/-/g, ":"); // Convert Windows format to standard } } if (match) { arpMap.set(match[1], match[2].toLowerCase()); } } } catch (error) { console.error("[DEBUG] Failed to get ARP table:", error); } return arpMap; } private isAnkerEufyDevice(mac: string): boolean { const macPrefix = mac.toLowerCase().substring(0, 8); return this.ANKER_EUFY_OUIS.some((oui) => macPrefix.startsWith(oui)); } private async pingHost(ip: string): Promise<boolean> { try { const platform = os.platform(); let pingCommand: string; if (platform === "win32") { pingCommand = `ping -n 1 -w 1000 ${ip}`; } else { pingCommand = `ping -c 1 -W 1 ${ip}`; } execSync(pingCommand, { encoding: "utf8", timeout: 2000, stdio: "pipe", // Suppress output }); return true; } catch { return false; } } async discoverDevices(): Promise<NetworkDevice[]> { console.error("[DEBUG] Starting local network discovery..."); const networkRange = this.getLocalNetworkRange(); console.error(`[DEBUG] Scanning network range: ${networkRange}`); // Get current ARP table const arpTable = await this.getArpTable(); console.error(`[DEBUG] Found ${arpTable.size} devices in ARP table`); // Generate IP range to scan const baseIp = networkRange.split("/")[0]; const ipParts = baseIp.split("."); const ips: string[] = []; // Scan common ranges more efficiently for (let i = 1; i < 255; i++) { ips.push(`${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.${i}`); } const devices: NetworkDevice[] = []; const batchSize = 20; // Process IPs in batches to avoid overwhelming the network for (let i = 0; i < ips.length; i += batchSize) { const batch = ips.slice(i, i + batchSize); const batchPromises = batch.map(async (ip) => { // First check if device responds to ping const isAlive = await this.pingHost(ip); if (!isAlive) return null; console.error(`[DEBUG] Device found at ${ip}, checking ports...`); // Check Tuya/Eufy ports const portResults = await Promise.all( this.TUYA_PORTS.map((port) => this.scanPort(ip, port, 500)) ); const openPorts = this.TUYA_PORTS.filter( (port, index) => portResults[index] ); if (openPorts.length === 0) return null; const mac = arpTable.get(ip); const isAnkerDevice = mac ? this.isAnkerEufyDevice(mac) : false; const device: NetworkDevice = { ip, mac, vendor: isAnkerDevice ? "Anker/Eufy" : undefined, ports: openPorts, isLikelyRoboVac: isAnkerDevice && openPorts.includes(6668), }; console.error(`[DEBUG] Potential device: ${JSON.stringify(device)}`); return device; }); const batchResults = await Promise.all(batchPromises); const validDevices = batchResults.filter( (device): device is NetworkDevice => device !== null ); devices.push(...validDevices); // Progress indicator console.error( `[DEBUG] Scanned ${Math.min(i + batchSize, ips.length)}/${ ips.length } IPs...` ); } // Sort by likelihood of being a RoboVac devices.sort((a, b) => { if (a.isLikelyRoboVac && !b.isLikelyRoboVac) return -1; if (!a.isLikelyRoboVac && b.isLikelyRoboVac) return 1; return 0; }); console.error( `[DEBUG] Network discovery complete. Found ${devices.length} potential devices` ); return devices; } async findRoboVacs(): Promise<NetworkDevice[]> { const allDevices = await this.discoverDevices(); // Filter for devices that are likely RoboVacs return allDevices.filter( (device) => device.isLikelyRoboVac || (device.ports.includes(6668) && device.vendor === "Anker/Eufy") ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/appleton/sam'

If you have feedback or need assistance with the MCP directory API, please join our Discord server