#!/usr/bin/env node /** * Query Omada Cloud Controller firewall rules for Blockscout access * Uses cloud controller API if credentials are available */ import https from 'https'; import { readFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; const BLOCKSCOUT_IP = '192.168.11.140'; const BLOCKSCOUT_PORT = '80'; // Load environment variables const envPath = join(homedir(), '.env'); let envVars = {}; try { const envFile = readFileSync(envPath, 'utf8'); envFile.split('\n').forEach(line => { if (line.includes('=') && !line.trim().startsWith('#')) { const [key, ...values] = line.split('='); if (key && /^[A-Z_][A-Z0-9_]*$/.test(key.trim())) { let value = values.join('=').trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } envVars[key.trim()] = value; } } }); } catch (error) { console.error('Error loading .env file:', error.message); process.exit(1); } // Try to detect cloud controller URL // Omada Cloud Controller typically uses: https://controller.tplinkcloud.com or specific domain const cloudControllerUrl = envVars.OMADA_CLOUD_CONTROLLER_URL || envVars.OMADA_CONTROLLER_URL || envVars.OMADA_CONTROLLER_BASE_URL || 'https://192.168.11.8:8043'; // Fallback to local // Check if this is a cloud URL (contains tplinkcloud.com or is not a local IP) const isCloudController = cloudControllerUrl.includes('tplinkcloud.com') || cloudControllerUrl.includes('cloud') || (!cloudControllerUrl.match(/^https?:\/\/192\.168\./) && !cloudControllerUrl.match(/^https?:\/\/10\./) && !cloudControllerUrl.match(/^https?:\/\/172\.(1[6-9]|2[0-9]|3[01])\./)); const username = envVars.OMADA_ADMIN_USERNAME || envVars.OMADA_API_KEY || envVars.OMADA_CLIENT_ID; const password = envVars.OMADA_ADMIN_PASSWORD || envVars.OMADA_API_SECRET || envVars.OMADA_CLIENT_SECRET; const siteId = envVars.OMADA_SITE_ID || '090862bebcb1997bb263eea9364957fe'; const verifySSL = envVars.OMADA_VERIFY_SSL !== 'false'; if (!username || !password) { console.error('Error: Missing credentials'); console.error('Required: OMADA_ADMIN_USERNAME/OMADA_API_KEY and OMADA_ADMIN_PASSWORD/OMADA_API_SECRET'); process.exit(1); } // Parse base URL const urlObj = new URL(cloudControllerUrl); const hostname = urlObj.hostname; const port = urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80); console.log('════════════════════════════════════════'); console.log('Omada Firewall Rules Check for Blockscout'); console.log(isCloudController ? '(Cloud Controller)' : '(Local Controller)'); console.log('════════════════════════════════════════'); console.log(''); console.log(`Controller URL: ${cloudControllerUrl}`); console.log(`Controller Type: ${isCloudController ? 'Cloud' : 'Local'}`); console.log(`Site ID: ${siteId}`); console.log(`Blockscout IP: ${BLOCKSCOUT_IP}`); console.log(`Blockscout Port: ${BLOCKSCOUT_PORT}`); console.log(''); // Create HTTPS agent const agent = new https.Agent({ rejectUnauthorized: verifySSL, }); // Function to make API request function apiRequest(method, path, data = null, token = null, cookies = null) { return new Promise((resolve, reject) => { const options = { hostname, port, path, method, agent, headers: { 'Content-Type': 'application/json', }, }; if (token) { options.headers['Csrf-Token'] = token; } if (cookies) { options.headers['Cookie'] = cookies; } const req = https.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { const json = JSON.parse(body); resolve(json); } catch (e) { resolve({ raw: body, statusCode: res.statusCode, headers: res.headers }); } }); }); req.on('error', (error) => { reject(error); }); if (data) { req.write(JSON.stringify(data)); } req.end(); }); } async function main() { try { console.log('1. Authenticating to Omada Controller...'); // Try login endpoint let loginResponse; try { loginResponse = await apiRequest('POST', '/api/v2/login', { username, password, }); } catch (error) { console.error(` ✗ Login failed: ${error.message}`); console.error(''); console.error('Note: Cloud controllers may use different authentication endpoints.'); console.error('Please check Omada Controller documentation for cloud API endpoints.'); process.exit(1); } if (loginResponse.errorCode !== 0) { console.error(` ✗ Login failed: ${loginResponse.msg || 'Unknown error'}`); console.error(` Error Code: ${loginResponse.errorCode}`); // If cloud controller, suggest alternative authentication if (isCloudController) { console.error(''); console.error('Cloud Controller Authentication Notes:'); console.error(' - Cloud controllers may require different authentication'); console.error(' - May need to use OAuth token endpoint instead of /api/v2/login'); console.error(' - Check Omada Cloud Controller documentation'); } process.exit(1); } const token = loginResponse.result?.token || loginResponse.token; if (!token) { console.error(' ✗ No token received'); process.exit(1); } console.log(' ✓ Login successful'); console.log(''); // Build cookie string for subsequent requests const cookies = `TOKEN=${token}`; const effectiveSiteId = siteId; console.log(`2. Querying firewall rules for site: ${effectiveSiteId}...`); console.log(''); // Try multiple endpoint paths for firewall rules const endpointPaths = [ `/api/v2/sites/${effectiveSiteId}/firewall/rules`, `/sites/${effectiveSiteId}/firewall/rules`, `/api/firewall/rules?siteId=${effectiveSiteId}`, `/api/v2/firewall/rules?siteId=${effectiveSiteId}`, ]; let rulesResponse; let rulesFound = false; for (const path of endpointPaths) { try { rulesResponse = await apiRequest('GET', path, null, token, cookies); // Check if we got a valid response if (rulesResponse.errorCode === 0 && Array.isArray(rulesResponse.result)) { rulesFound = true; break; } else if (Array.isArray(rulesResponse)) { rulesResponse = { errorCode: 0, result: rulesResponse }; rulesFound = true; break; } } catch (e) { continue; } } if (!rulesFound) { console.error(` ✗ Could not query firewall rules via API`); console.error(''); console.error('Note: Firewall rules may need to be checked via Omada Controller web interface:'); console.error(` ${cloudControllerUrl}`); console.error(' Navigate to: Settings → Firewall → Firewall Rules'); console.error(''); console.error('Or firewall rules API may not be available for this controller type.'); process.exit(1); } const rules = Array.isArray(rulesResponse.result) ? rulesResponse.result : []; console.log(` ✓ Found ${rules.length} firewall rules`); console.log(''); // Filter rules that might affect Blockscout const relevantRules = rules.filter((rule) => { const affectsBlockscoutIP = !rule.dstIp || rule.dstIp === BLOCKSCOUT_IP || (typeof rule.dstIp === 'string' && rule.dstIp.includes('192.168.11')) || rule.dstIp === '192.168.11.0/24'; const affectsPort80 = !rule.dstPort || rule.dstPort === BLOCKSCOUT_PORT || (typeof rule.dstPort === 'string' && rule.dstPort.includes(BLOCKSCOUT_PORT)) || rule.dstPort === 'all' || rule.dstPort === '0-65535'; const isTCP = !rule.protocol || rule.protocol === 'tcp' || rule.protocol === 'tcp/udp' || rule.protocol === 'all'; return rule.enable && (affectsBlockscoutIP || affectsPort80) && isTCP; }); if (relevantRules.length > 0) { console.log('════════════════════════════════════════'); console.log(`🔍 Found ${relevantRules.length} rule(s) that might affect Blockscout:`); console.log('════════════════════════════════════════'); console.log(''); relevantRules.forEach((rule, index) => { console.log(`Rule ${index + 1}: ${rule.name || rule.id || 'Unnamed'}`); console.log(` ID: ${rule.id || 'N/A'}`); console.log(` Enabled: ${rule.enable ? 'Yes ✓' : 'No ✗'}`); console.log(` Action: ${rule.action || 'N/A'}`); console.log(` Direction: ${rule.direction || 'N/A'}`); console.log(` Protocol: ${rule.protocol || 'all'}`); console.log(` Source IP: ${rule.srcIp || 'Any'}`); console.log(` Source Port: ${rule.srcPort || 'Any'}`); console.log(` Destination IP: ${rule.dstIp || 'Any'}`); console.log(` Destination Port: ${rule.dstPort || 'Any'}`); console.log(` Priority: ${rule.priority !== undefined ? rule.priority : 'N/A'}`); console.log(''); if (rule.action === 'deny' || rule.action === 'reject') { console.log(' ⚠️ WARNING: This rule BLOCKS traffic!'); console.log(''); } }); // Separate allow and deny rules const allowRules = relevantRules.filter((rule) => rule.action === 'allow'); const denyRules = relevantRules.filter((rule) => rule.action === 'deny' || rule.action === 'reject'); console.log('════════════════════════════════════════'); console.log('Analysis'); console.log('════════════════════════════════════════'); console.log(''); if (denyRules.length > 0 && allowRules.length === 0) { console.log('❌ Issue Found:'); console.log(' Deny rules exist that block Blockscout, but no allow rules found.'); console.log(' This explains the "No route to host" error.'); console.log(''); console.log('✅ Recommended Action:'); console.log(' Create an allow rule in Omada Controller with HIGH priority:'); console.log(''); console.log(' Name: Allow Internal to Blockscout HTTP'); console.log(' Enable: Yes'); console.log(' Action: Allow'); console.log(' Direction: Forward'); console.log(' Protocol: TCP'); console.log(' Source IP: 192.168.11.0/24 (or leave blank for Any)'); console.log(' Destination IP: 192.168.11.140'); console.log(' Destination Port: 80'); console.log(' Priority: High (above deny rules)'); console.log(''); } else if (allowRules.length > 0 && denyRules.length > 0) { const highestAllowPriority = Math.max(...allowRules.map((r) => r.priority || 0)); const lowestDenyPriority = Math.min(...denyRules.map((r) => r.priority || 9999)); if (highestAllowPriority < lowestDenyPriority) { console.log('✅ Priority order looks correct (allow rules above deny rules).'); } else { console.log('❌ Issue: Some deny rules have higher priority than allow rules.'); console.log(' Adjust rule priority so allow rules are above deny rules.'); } console.log(''); } else if (allowRules.length > 0) { console.log('✅ Allow rules exist for Blockscout.'); console.log(' If issues persist, check rule priority or default policies.'); console.log(''); } } else { console.log('════════════════════════════════════════'); console.log('ℹ️ No firewall rules found that specifically target Blockscout.'); console.log('════════════════════════════════════════'); console.log(''); // Check for deny rules const denyRules = rules.filter( (rule) => rule.enable && (rule.action === 'deny' || rule.action === 'reject') ); if (denyRules.length > 0) { console.log(`⚠️ Found ${denyRules.length} deny/reject rules in total:`); console.log(''); denyRules.slice(0, 10).forEach((rule) => { console.log(` - ${rule.name || rule.id} (Priority: ${rule.priority || 'N/A'})`); if (rule.dstIp) console.log(` Dest IP: ${rule.dstIp}`); if (rule.dstPort) console.log(` Dest Port: ${rule.dstPort}`); }); if (denyRules.length > 10) { console.log(` ... and ${denyRules.length - 10} more`); } console.log(''); } console.log('✅ Recommendation:'); console.log(' Create an explicit allow rule to ensure Blockscout access.'); console.log(''); } // Show all rules summary console.log('════════════════════════════════════════'); console.log('All Firewall Rules Summary'); console.log('════════════════════════════════════════'); console.log(''); const enabledRules = rules.filter((r) => r.enable); const allowCount = enabledRules.filter((r) => r.action === 'allow').length; const denyCount = enabledRules.filter((r) => r.action === 'deny' || r.action === 'reject').length; console.log(`Total Rules: ${rules.length}`); console.log(` Enabled: ${enabledRules.length}`); console.log(` Allow Actions: ${allowCount}`); console.log(` Deny/Reject Actions: ${denyCount}`); console.log(''); console.log('════════════════════════════════════════'); } catch (error) { console.error('\n❌ Error:'); console.error(''); if (error.message) { console.error(` ${error.message}`); } else { console.error(' ', error); } if (error.stack) { console.error(''); console.error('Stack trace:'); console.error(error.stack); } process.exit(1); } } main();