#!/usr/bin/env node /** * List current ACL rules from UniFi Network API * Helps diagnose what might be blocking access to VLAN 11 */ import { readFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; // Load environment variables const envPath = join(homedir(), '.env'); function loadEnvFile(filePath) { try { const envFile = readFileSync(filePath, 'utf8'); const envVars = envFile.split('\n').filter( (line) => line.includes('=') && !line.trim().startsWith('#') ); for (const line of envVars) { const [key, ...values] = line.split('='); if (key && values.length > 0 && /^[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); } process.env[key.trim()] = value; } } return true; } catch { return false; } } loadEnvFile(envPath); const baseUrl = process.env.UNIFI_UDM_URL || 'https://192.168.0.1'; const apiKey = process.env.UNIFI_API_KEY; const siteId = '88f7af54-98f8-306a-a1c7-c9349722b1f6'; if (!apiKey) { console.error('❌ UNIFI_API_KEY not set in environment'); process.exit(1); } // Fetch networks to map IDs to VLANs async function fetchNetworks() { const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/networks`, { headers: { 'X-API-KEY': apiKey, 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`Failed to fetch networks: ${response.status} ${response.statusText}`); } const data = await response.json(); const networks = data.data || []; const networkMap = {}; for (const net of networks) { networkMap[net.id] = { vlanId: net.vlanId, name: net.name, subnet: net.ipSubnet, }; } return networkMap; } // List ACL rules async function listACLRules() { const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/acl-rules`, { headers: { 'X-API-KEY': apiKey, 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`Failed to fetch ACL rules: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.data || []; } // Main function async function main() { try { console.log('Fetching ACL Rules from UniFi Network API'); console.log('=========================================='); console.log(''); // Fetch networks and ACL rules in parallel const [networkMap, rules] = await Promise.all([ fetchNetworks(), listACLRules(), ]); console.log(`Found ${rules.length} ACL rules`); console.log(''); if (rules.length === 0) { console.log('No ACL rules configured. Default policy applies.'); return; } // Find VLAN 11 network ID const vlan11Network = Object.values(networkMap).find(n => n.vlanId === 11); const vlan11Id = vlan11Network ? Object.keys(networkMap).find(id => networkMap[id].vlanId === 11) : null; console.log('='.repeat(80)); console.log('ALL ACL RULES (sorted by priority/index)'); console.log('='.repeat(80)); console.log(''); // Sort rules by index (lower index = higher priority) const sortedRules = [...rules].sort((a, b) => (a.index || 9999) - (b.index || 9999)); for (const rule of sortedRules) { const enabled = rule.enabled ? '✅' : '❌'; const action = rule.action === 'BLOCK' ? '🚫 BLOCK' : '✅ ALLOW'; console.log(`${enabled} ${action} - ${rule.name || 'Unnamed Rule'}`); console.log(` Priority/Index: ${rule.index ?? 'N/A'}`); if (rule.description) { console.log(` Description: ${rule.description}`); } // Parse source filter if (rule.sourceFilter) { if (rule.sourceFilter.type === 'NETWORKS' && rule.sourceFilter.networkIds) { const sourceNetworks = rule.sourceFilter.networkIds .map(id => { const net = networkMap[id]; return net ? `VLAN ${net.vlanId} (${net.name})` : id; }) .join(', '); console.log(` Source: ${sourceNetworks}`); } else if (rule.sourceFilter.type === 'NETWORK' && rule.sourceFilter.networkIds) { const sourceNetworks = rule.sourceFilter.networkIds .map(id => { const net = networkMap[id]; return net ? `VLAN ${net.vlanId} (${net.name})` : id; }) .join(', '); console.log(` Source: ${sourceNetworks}`); } else if (rule.sourceFilter.type === 'IP_ADDRESS') { console.log(` Source: IP Address Filter`); } else { console.log(` Source: ${JSON.stringify(rule.sourceFilter)}`); } } else { console.log(` Source: Any`); } // Parse destination filter if (rule.destinationFilter) { if (rule.destinationFilter.type === 'NETWORKS' && rule.destinationFilter.networkIds) { const destNetworks = rule.destinationFilter.networkIds .map(id => { const net = networkMap[id]; return net ? `VLAN ${net.vlanId} (${net.name})` : id; }) .join(', '); console.log(` Destination: ${destNetworks}`); } else if (rule.destinationFilter.type === 'NETWORK' && rule.destinationFilter.networkIds) { const destNetworks = rule.destinationFilter.networkIds .map(id => { const net = networkMap[id]; return net ? `VLAN ${net.vlanId} (${net.name})` : id; }) .join(', '); console.log(` Destination: ${destNetworks}`); } else if (rule.destinationFilter.type === 'IP_ADDRESS') { console.log(` Destination: IP Address Filter`); } else { console.log(` Destination: ${JSON.stringify(rule.destinationFilter)}`); } } else { console.log(` Destination: Any`); } // Protocol filter if (rule.protocolFilter && rule.protocolFilter.length > 0) { console.log(` Protocol: ${rule.protocolFilter.join(', ')}`); } else { console.log(` Protocol: Any`); } // Port filter if (rule.portFilter) { if (rule.portFilter.type === 'PORTS' && rule.portFilter.ports) { console.log(` Ports: ${rule.portFilter.ports.join(', ')}`); } else { console.log(` Ports: ${JSON.stringify(rule.portFilter)}`); } } console.log(''); } // Analyze rules affecting VLAN 11 if (vlan11Id) { console.log('='.repeat(80)); console.log('RULES AFFECTING VLAN 11 (MGMT-LAN)'); console.log('='.repeat(80)); console.log(''); const vlan11Rules = sortedRules.filter(rule => { if (!rule.enabled) return false; // Check if rule affects VLAN 11 as source const affectsSource = rule.sourceFilter && ((rule.sourceFilter.type === 'NETWORKS' || rule.sourceFilter.type === 'NETWORK') && rule.sourceFilter.networkIds && rule.sourceFilter.networkIds.includes(vlan11Id)); // Check if rule affects VLAN 11 as destination const affectsDest = rule.destinationFilter && ((rule.destinationFilter.type === 'NETWORKS' || rule.destinationFilter.type === 'NETWORK') && rule.destinationFilter.networkIds && rule.destinationFilter.networkIds.includes(vlan11Id)); return affectsSource || affectsDest; }); if (vlan11Rules.length === 0) { console.log('No specific rules found affecting VLAN 11.'); console.log('Default policy applies (typically ALLOW for inter-VLAN traffic).'); } else { for (const rule of vlan11Rules) { const action = rule.action === 'BLOCK' ? '🚫 BLOCKS' : '✅ ALLOWS'; const direction = rule.sourceFilter?.networkIds?.includes(vlan11Id) ? 'FROM VLAN 11' : rule.destinationFilter?.networkIds?.includes(vlan11Id) ? 'TO VLAN 11' : 'AFFECTING VLAN 11'; console.log(`${action} ${direction}: ${rule.name || 'Unnamed Rule'}`); console.log(` Priority: ${rule.index ?? 'N/A'}`); if (rule.description) { console.log(` ${rule.description}`); } console.log(''); } } } // Summary console.log('='.repeat(80)); console.log('SUMMARY'); console.log('='.repeat(80)); console.log(''); console.log(`Total Rules: ${rules.length}`); console.log(`Enabled Rules: ${rules.filter(r => r.enabled).length}`); console.log(`Block Rules: ${rules.filter(r => r.action === 'BLOCK' && r.enabled).length}`); console.log(`Allow Rules: ${rules.filter(r => r.action === 'ALLOW' && r.enabled).length}`); console.log(''); } catch (error) { console.error('❌ Error:', error.message); if (error.stack) { console.error(error.stack); } process.exit(1); } } main();