Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
406 lines
17 KiB
JavaScript
Executable File
406 lines
17 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
/**
|
||
* Verify VLAN Settings - Network Isolation and Zone Matrix
|
||
* Automates verification of critical VLAN settings on UDM Pro
|
||
*/
|
||
|
||
import { chromium } from 'playwright';
|
||
import { readFileSync, existsSync } from 'fs';
|
||
import { join } from 'path';
|
||
import { homedir } from 'os';
|
||
|
||
// Load environment variables
|
||
const envFile = join(homedir(), '.env');
|
||
let env = {};
|
||
if (existsSync(envFile)) {
|
||
readFileSync(envFile, 'utf8').split('\n').forEach(line => {
|
||
const match = line.match(/^([^=]+)=(.*)$/);
|
||
if (match) {
|
||
const key = match[1].trim();
|
||
const value = match[2].trim().replace(/^['"]|['"]$/g, '');
|
||
env[key] = value;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Configuration
|
||
const UDM_PRO_URL = env.UNIFI_UDM_URL || 'https://192.168.0.1';
|
||
const USERNAME = env.UNIFI_USERNAME || 'unifi_api';
|
||
const PASSWORD = env.UNIFI_PASSWORD || '';
|
||
const HEADLESS = env.HEADLESS !== 'false';
|
||
const TIMEOUT = 60000;
|
||
|
||
// VLAN list to verify
|
||
const VLAN_LIST = [
|
||
{ name: 'Default', vlanId: 1, subnet: '192.168.0.0/24' },
|
||
{ name: 'MGMT-LAN', vlanId: 11, subnet: '192.168.11.0/24' },
|
||
{ name: 'BESU-VAL', vlanId: 110, subnet: '10.110.0.0/24' },
|
||
{ name: 'BESU-SEN', vlanId: 111, subnet: '10.111.0.0/24' },
|
||
{ name: 'BESU-RPC', vlanId: 112, subnet: '10.112.0.0/24' },
|
||
{ name: 'BLOCKSCOUT', vlanId: 120, subnet: '10.120.0.0/24' },
|
||
{ name: 'CACTI', vlanId: 121, subnet: '10.121.0.0/24' },
|
||
{ name: 'CCIP-OPS', vlanId: 130, subnet: '10.130.0.0/24' },
|
||
{ name: 'CCIP-COMMIT', vlanId: 132, subnet: '10.132.0.0/24' },
|
||
{ name: 'CCIP-EXEC', vlanId: 133, subnet: '10.133.0.0/24' },
|
||
{ name: 'CCIP-RMN', vlanId: 134, subnet: '10.134.0.0/24' },
|
||
{ name: 'FABRIC', vlanId: 140, subnet: '10.140.0.0/24' },
|
||
{ name: 'FIREFLY', vlanId: 141, subnet: '10.141.0.0/24' },
|
||
{ name: 'INDY', vlanId: 150, subnet: '10.150.0.0/24' },
|
||
{ name: 'SANKOFA-SVC', vlanId: 160, subnet: '10.160.0.0/22' },
|
||
{ name: 'PHX-SOV-SMOM', vlanId: 200, subnet: '10.200.0.0/20' },
|
||
{ name: 'PHX-SOV-ICCC', vlanId: 201, subnet: '10.201.0.0/20' },
|
||
{ name: 'PHX-SOV-DBIS', vlanId: 202, subnet: '10.202.0.0/24' },
|
||
{ name: 'PHX-SOV-AR', vlanId: 203, subnet: '10.203.0.0/20' },
|
||
];
|
||
|
||
const log = (message) => {
|
||
const timestamp = new Date().toISOString();
|
||
console.log(`[${timestamp}] ${message}`);
|
||
};
|
||
|
||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||
|
||
async function login(page) {
|
||
log('🔐 Logging in to UDM Pro...');
|
||
|
||
try {
|
||
await page.goto(UDM_PRO_URL, { waitUntil: 'networkidle', timeout: TIMEOUT });
|
||
await sleep(2000);
|
||
|
||
// Check if already logged in
|
||
const currentUrl = page.url();
|
||
if (!currentUrl.includes('/login')) {
|
||
log('✅ Already logged in');
|
||
return true;
|
||
}
|
||
|
||
// Fill login form
|
||
log('Filling login form...');
|
||
await page.fill('input[type="text"], input[name="username"], input[placeholder*="username" i], input[placeholder*="email" i]', USERNAME);
|
||
await page.fill('input[type="password"], input[name="password"]', PASSWORD);
|
||
|
||
// Click login button
|
||
await page.click('button[type="submit"], button:has-text("Sign In"), button:has-text("Login")');
|
||
await sleep(3000);
|
||
|
||
// Wait for navigation
|
||
await page.waitForURL(/^(?!.*login)/, { timeout: 10000 });
|
||
log('✅ Login successful');
|
||
return true;
|
||
} catch (error) {
|
||
log(`❌ Login failed: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function verifyNetworkIsolation(page, vlan) {
|
||
log(`\n🔍 Verifying Network Isolation for ${vlan.name} (VLAN ${vlan.vlanId})...`);
|
||
|
||
try {
|
||
// Navigate to Networks
|
||
log('Navigating to Networks...');
|
||
await page.goto(`${UDM_PRO_URL}/network/default/settings/networks`, { waitUntil: 'networkidle', timeout: TIMEOUT });
|
||
await sleep(2000);
|
||
|
||
// Find and click on the VLAN
|
||
log(`Looking for ${vlan.name}...`);
|
||
const vlanRow = page.locator(`text=${vlan.name}`).first();
|
||
await vlanRow.waitFor({ timeout: 10000 });
|
||
await vlanRow.click();
|
||
await sleep(2000);
|
||
|
||
// Check for Network Isolation checkbox
|
||
log('Checking Network Isolation setting...');
|
||
|
||
// Try multiple selectors for the checkbox
|
||
const isolationSelectors = [
|
||
'input[type="checkbox"][name*="isolate" i]',
|
||
'input[type="checkbox"][id*="isolate" i]',
|
||
'label:has-text("Isolate Network") input[type="checkbox"]',
|
||
'input[type="checkbox"]:near(:text("Isolate Network"))',
|
||
];
|
||
|
||
let isolationChecked = null;
|
||
for (const selector of isolationSelectors) {
|
||
try {
|
||
const checkbox = page.locator(selector).first();
|
||
if (await checkbox.count() > 0) {
|
||
isolationChecked = await checkbox.isChecked();
|
||
log(`Found Network Isolation checkbox: ${isolationChecked ? 'CHECKED ❌' : 'UNCHECKED ✅'}`);
|
||
break;
|
||
}
|
||
} catch (e) {
|
||
// Continue to next selector
|
||
}
|
||
}
|
||
|
||
if (isolationChecked === null) {
|
||
// Try to find text and check nearby checkbox
|
||
const isolateText = page.locator('text=/isolate.*network/i').first();
|
||
if (await isolateText.count() > 0) {
|
||
const nearbyCheckbox = isolateText.locator('..').locator('input[type="checkbox"]').first();
|
||
if (await nearbyCheckbox.count() > 0) {
|
||
isolationChecked = await nearbyCheckbox.isChecked();
|
||
log(`Found Network Isolation checkbox (near text): ${isolationChecked ? 'CHECKED ❌' : 'UNCHECKED ✅'}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (isolationChecked === null) {
|
||
log('⚠️ Could not find Network Isolation checkbox');
|
||
return { vlan: vlan.name, isolation: 'unknown', error: 'Checkbox not found' };
|
||
}
|
||
|
||
return {
|
||
vlan: vlan.name,
|
||
vlanId: vlan.vlanId,
|
||
isolation: isolationChecked ? 'enabled' : 'disabled',
|
||
status: isolationChecked ? '❌ NEEDS FIX' : '✅ OK'
|
||
};
|
||
|
||
} catch (error) {
|
||
log(`❌ Error verifying ${vlan.name}: ${error.message}`);
|
||
return { vlan: vlan.name, isolation: 'error', error: error.message };
|
||
}
|
||
}
|
||
|
||
async function verifyZoneMatrix(page) {
|
||
log('\n🔍 Verifying Zone Matrix configuration...');
|
||
|
||
try {
|
||
// Navigate to Policy Engine / Zone Matrix
|
||
log('Navigating to Policy Engine...');
|
||
await page.goto(`${UDM_PRO_URL}/network/default/settings/policy-engine`, { waitUntil: 'networkidle', timeout: TIMEOUT });
|
||
await sleep(3000);
|
||
|
||
// Look for Zone Matrix
|
||
log('Looking for Zone Matrix...');
|
||
|
||
// Try to find "Internal" → "Internal" cell
|
||
const internalText = page.locator('text=/internal/i').first();
|
||
if (await internalText.count() > 0) {
|
||
log('Found Internal zone reference');
|
||
}
|
||
|
||
// Look for grid/table with zone matrix
|
||
const matrixSelectors = [
|
||
'text=/allow.*all/i',
|
||
'text=/internal.*internal/i',
|
||
'[data-testid*="zone"]',
|
||
'.zone-matrix',
|
||
];
|
||
|
||
let zoneMatrixStatus = null;
|
||
for (const selector of matrixSelectors) {
|
||
try {
|
||
const element = page.locator(selector).first();
|
||
if (await element.count() > 0) {
|
||
const text = await element.textContent();
|
||
log(`Found zone matrix element: ${text}`);
|
||
if (text && text.toLowerCase().includes('allow all')) {
|
||
zoneMatrixStatus = 'allow_all';
|
||
}
|
||
}
|
||
} catch (e) {
|
||
// Continue
|
||
}
|
||
}
|
||
|
||
// Try JavaScript evaluation to find zone matrix
|
||
try {
|
||
const zoneMatrixInfo = await page.evaluate(() => {
|
||
// Look for elements containing "Internal" and "Allow"
|
||
const elements = Array.from(document.querySelectorAll('*'));
|
||
for (const el of elements) {
|
||
const text = el.textContent || '';
|
||
if (text.includes('Internal') && text.includes('Allow')) {
|
||
return { found: true, text: text.substring(0, 100) };
|
||
}
|
||
}
|
||
return { found: false };
|
||
});
|
||
|
||
if (zoneMatrixInfo.found) {
|
||
log(`Found zone matrix reference: ${zoneMatrixInfo.text}`);
|
||
if (zoneMatrixInfo.text.toLowerCase().includes('allow all')) {
|
||
zoneMatrixStatus = 'allow_all';
|
||
}
|
||
}
|
||
} catch (e) {
|
||
log(`JavaScript evaluation failed: ${e.message}`);
|
||
}
|
||
|
||
if (zoneMatrixStatus === 'allow_all') {
|
||
return { status: '✅ OK', configured: true };
|
||
} else {
|
||
return { status: '⚠️ NEEDS VERIFICATION', configured: false };
|
||
}
|
||
|
||
} catch (error) {
|
||
log(`❌ Error verifying Zone Matrix: ${error.message}`);
|
||
return { status: 'error', error: error.message };
|
||
}
|
||
}
|
||
|
||
async function testInterVlanRouting() {
|
||
log('\n🧪 Testing Inter-VLAN Routing...');
|
||
|
||
const gateways = [
|
||
{ ip: '10.110.0.1', name: 'BESU-VAL (VLAN 110)' },
|
||
{ ip: '10.111.0.1', name: 'BESU-SEN (VLAN 111)' },
|
||
{ ip: '10.112.0.1', name: 'BESU-RPC (VLAN 112)' },
|
||
{ ip: '10.120.0.1', name: 'BLOCKSCOUT (VLAN 120)' },
|
||
{ ip: '10.121.0.1', name: 'CACTI (VLAN 121)' },
|
||
{ ip: '10.130.0.1', name: 'CCIP-OPS (VLAN 130)' },
|
||
{ ip: '10.132.0.1', name: 'CCIP-COMMIT (VLAN 132)' },
|
||
{ ip: '10.133.0.1', name: 'CCIP-EXEC (VLAN 133)' },
|
||
{ ip: '10.134.0.1', name: 'CCIP-RMN (VLAN 134)' },
|
||
{ ip: '10.140.0.1', name: 'FABRIC (VLAN 140)' },
|
||
{ ip: '10.141.0.1', name: 'FIREFLY (VLAN 141)' },
|
||
{ ip: '10.150.0.1', name: 'INDY (VLAN 150)' },
|
||
{ ip: '10.160.0.1', name: 'SANKOFA-SVC (VLAN 160)' },
|
||
{ ip: '10.200.0.1', name: 'PHX-SOV-SMOM (VLAN 200)' },
|
||
{ ip: '10.201.0.1', name: 'PHX-SOV-ICCC (VLAN 201)' },
|
||
{ ip: '10.202.0.1', name: 'PHX-SOV-DBIS (VLAN 202)' },
|
||
{ ip: '10.203.0.1', name: 'PHX-SOV-AR (VLAN 203)' },
|
||
];
|
||
|
||
const results = [];
|
||
let reachable = 0;
|
||
let unreachable = 0;
|
||
|
||
const { execSync } = await import('child_process');
|
||
for (const gateway of gateways) {
|
||
try {
|
||
execSync(`ping -c 1 -W 2 ${gateway.ip}`, { stdio: 'ignore', timeout: 3000 });
|
||
log(` ✅ ${gateway.name} (${gateway.ip}) - REACHABLE`);
|
||
results.push({ ...gateway, reachable: true });
|
||
reachable++;
|
||
} catch (e) {
|
||
log(` ❌ ${gateway.name} (${gateway.ip}) - UNREACHABLE`);
|
||
results.push({ ...gateway, reachable: false });
|
||
unreachable++;
|
||
}
|
||
}
|
||
|
||
log(`\n📊 Routing Test Results: ${reachable} reachable, ${unreachable} unreachable`);
|
||
|
||
return { results, reachable, unreachable };
|
||
}
|
||
|
||
async function main() {
|
||
log('🚀 Starting VLAN Settings Verification');
|
||
log(`UDM Pro URL: ${UDM_PRO_URL}`);
|
||
log(`Headless: ${HEADLESS}`);
|
||
log('');
|
||
|
||
if (!PASSWORD) {
|
||
log('❌ UNIFI_PASSWORD not set. Please set it in ~/.env');
|
||
process.exit(1);
|
||
}
|
||
|
||
const browser = await chromium.launch({
|
||
headless: HEADLESS,
|
||
ignoreHTTPSErrors: true,
|
||
});
|
||
|
||
const context = await browser.newContext({
|
||
ignoreHTTPSErrors: true,
|
||
});
|
||
|
||
const page = await context.newPage();
|
||
|
||
try {
|
||
// Login
|
||
const loginSuccess = await login(page);
|
||
if (!loginSuccess) {
|
||
log('❌ Failed to login. Exiting.');
|
||
await browser.close();
|
||
process.exit(1);
|
||
}
|
||
|
||
// Verify Network Isolation for all VLANs
|
||
log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
log('1️⃣ VERIFYING NETWORK ISOLATION');
|
||
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
|
||
const isolationResults = [];
|
||
for (const vlan of VLAN_LIST) {
|
||
const result = await verifyNetworkIsolation(page, vlan);
|
||
isolationResults.push(result);
|
||
await sleep(1000); // Brief pause between VLANs
|
||
}
|
||
|
||
// Verify Zone Matrix
|
||
log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
log('2️⃣ VERIFYING ZONE MATRIX');
|
||
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
|
||
const zoneMatrixResult = await verifyZoneMatrix(page);
|
||
|
||
// Test Inter-VLAN Routing
|
||
log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
log('3️⃣ TESTING INTER-VLAN ROUTING');
|
||
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
|
||
const routingResults = await testInterVlanRouting();
|
||
|
||
// Summary
|
||
log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
log('📊 VERIFICATION SUMMARY');
|
||
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
|
||
log('\n1. Network Isolation:');
|
||
const isolationOk = isolationResults.filter(r => r.isolation === 'disabled').length;
|
||
const isolationNeedsFix = isolationResults.filter(r => r.isolation === 'enabled').length;
|
||
const isolationUnknown = isolationResults.filter(r => r.isolation === 'unknown' || r.isolation === 'error').length;
|
||
|
||
log(` ✅ Correctly disabled: ${isolationOk}/${VLAN_LIST.length}`);
|
||
log(` ❌ Needs fix (enabled): ${isolationNeedsFix}`);
|
||
log(` ⚠️ Unknown/Error: ${isolationUnknown}`);
|
||
|
||
if (isolationNeedsFix > 0) {
|
||
log('\n VLANs that need Network Isolation disabled:');
|
||
isolationResults.filter(r => r.isolation === 'enabled').forEach(r => {
|
||
log(` • ${r.vlan} (VLAN ${r.vlanId})`);
|
||
});
|
||
}
|
||
|
||
log('\n2. Zone Matrix:');
|
||
log(` ${zoneMatrixResult.status}`);
|
||
if (!zoneMatrixResult.configured) {
|
||
log(' ⚠️ Please verify manually: Policy Engine → Zone Matrix → Internal → Internal = Allow All');
|
||
}
|
||
|
||
log('\n3. Inter-VLAN Routing:');
|
||
log(` ✅ Reachable: ${routingResults.reachable}`);
|
||
log(` ❌ Unreachable: ${routingResults.unreachable}`);
|
||
|
||
if (routingResults.unreachable > 0) {
|
||
log('\n Unreachable gateways:');
|
||
routingResults.results.filter(r => !r.reachable).forEach(r => {
|
||
log(` • ${r.name} (${r.ip})`);
|
||
});
|
||
log('\n 💡 If routing is not working, ensure:');
|
||
log(' • Network Isolation is disabled for all VLANs');
|
||
log(' • Zone Matrix: Internal → Internal = Allow All');
|
||
}
|
||
|
||
log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
log('✅ Verification Complete');
|
||
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||
|
||
} catch (error) {
|
||
log(`❌ Error: ${error.message}`);
|
||
console.error(error);
|
||
} finally {
|
||
if (!HEADLESS) {
|
||
log('\n⏸️ Browser will remain open for 30 seconds for inspection...');
|
||
await sleep(30000);
|
||
}
|
||
await browser.close();
|
||
}
|
||
}
|
||
|
||
main().catch(console.error);
|