#!/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);