390 lines
15 KiB
JavaScript
390 lines
15 KiB
JavaScript
|
|
#!/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();
|
|||
|
|
|