204 lines
5.6 KiB
JavaScript
204 lines
5.6 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Create firewall rules via UniFi Network API
|
||
|
|
* Node.js version for better JSON handling
|
||
|
|
*/
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('Creating Firewall Rules via API');
|
||
|
|
console.log('==================================');
|
||
|
|
console.log('');
|
||
|
|
console.log(`UDM URL: ${baseUrl}`);
|
||
|
|
console.log(`Site ID: ${siteId}`);
|
||
|
|
console.log('');
|
||
|
|
|
||
|
|
// Fetch networks
|
||
|
|
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 || [];
|
||
|
|
|
||
|
|
// Create VLAN to network ID mapping
|
||
|
|
const vlanMap = {};
|
||
|
|
for (const net of networks) {
|
||
|
|
const vlanId = net.vlanId;
|
||
|
|
if (vlanId && vlanId > 1) {
|
||
|
|
vlanMap[vlanId] = {
|
||
|
|
id: net.id,
|
||
|
|
name: net.name,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return vlanMap;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create ACL rule
|
||
|
|
async function createACLRule(ruleConfig) {
|
||
|
|
const { name, description, action, index, sourceNetworks, destNetworks, protocolFilter } = ruleConfig;
|
||
|
|
|
||
|
|
const rule = {
|
||
|
|
type: 'IPV4',
|
||
|
|
enabled: true,
|
||
|
|
name,
|
||
|
|
description,
|
||
|
|
action,
|
||
|
|
index,
|
||
|
|
sourceFilter: sourceNetworks && sourceNetworks.length > 0
|
||
|
|
? { type: 'NETWORKS', networkIds: sourceNetworks }
|
||
|
|
: null,
|
||
|
|
destinationFilter: destNetworks && destNetworks.length > 0
|
||
|
|
? { type: 'NETWORKS', networkIds: destNetworks }
|
||
|
|
: null,
|
||
|
|
protocolFilter: protocolFilter || null,
|
||
|
|
enforcingDeviceFilter: null,
|
||
|
|
};
|
||
|
|
|
||
|
|
const response = await fetch(`${baseUrl}/proxy/network/integration/v1/sites/${siteId}/acl-rules`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'X-API-KEY': apiKey,
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Accept': 'application/json',
|
||
|
|
},
|
||
|
|
body: JSON.stringify(rule),
|
||
|
|
});
|
||
|
|
|
||
|
|
const responseText = await response.text();
|
||
|
|
let responseData;
|
||
|
|
try {
|
||
|
|
responseData = JSON.parse(responseText);
|
||
|
|
} catch {
|
||
|
|
responseData = responseText;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
return { success: true, data: responseData };
|
||
|
|
} else {
|
||
|
|
return { success: false, error: responseData, status: response.status };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Main function
|
||
|
|
async function main() {
|
||
|
|
try {
|
||
|
|
console.log('Fetching network IDs...');
|
||
|
|
const vlanMap = await fetchNetworks();
|
||
|
|
|
||
|
|
// Key VLANs we need
|
||
|
|
const keyVlans = [11, 110, 111, 112, 120, 121, 130, 132, 133, 134, 140, 141, 150, 160, 200, 201, 202, 203];
|
||
|
|
console.log('Network ID Mapping:');
|
||
|
|
console.log('='.repeat(80));
|
||
|
|
for (const vlan of keyVlans.sort((a, b) => a - b)) {
|
||
|
|
if (vlanMap[vlan]) {
|
||
|
|
console.log(`VLAN ${String(vlan).padStart(3)} (${vlanMap[vlan].name.padEnd(15)}): ${vlanMap[vlan].id}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
console.log('');
|
||
|
|
|
||
|
|
// Create firewall rules
|
||
|
|
console.log('Creating firewall rules...');
|
||
|
|
console.log('');
|
||
|
|
|
||
|
|
const rules = [];
|
||
|
|
|
||
|
|
// Rule 1: Block sovereign tenant inter-VLAN traffic (VLANs 200-203)
|
||
|
|
const sovereignVlans = [200, 201, 202, 203].map(v => vlanMap[v]?.id).filter(Boolean);
|
||
|
|
if (sovereignVlans.length === 4) {
|
||
|
|
rules.push({
|
||
|
|
name: 'Block Sovereign Tenant East-West Traffic',
|
||
|
|
description: 'Deny traffic between sovereign tenant VLANs (200-203) for isolation',
|
||
|
|
action: 'BLOCK',
|
||
|
|
index: 100,
|
||
|
|
sourceNetworks: sovereignVlans,
|
||
|
|
destNetworks: sovereignVlans,
|
||
|
|
protocolFilter: null,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create rules
|
||
|
|
for (const ruleConfig of rules) {
|
||
|
|
console.log(`Creating rule: ${ruleConfig.name}`);
|
||
|
|
const result = await createACLRule(ruleConfig);
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
console.log(' ✅ Rule created successfully');
|
||
|
|
} else {
|
||
|
|
console.log(` ❌ Failed to create rule (HTTP ${result.status})`);
|
||
|
|
console.log(` Error: ${JSON.stringify(result.error, null, 2)}`);
|
||
|
|
}
|
||
|
|
console.log('');
|
||
|
|
|
||
|
|
// Small delay between requests
|
||
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('✅ Firewall rule creation complete!');
|
||
|
|
console.log('');
|
||
|
|
console.log('Note: Additional rules can be added for:');
|
||
|
|
console.log(' - Management VLAN access (VLAN 11 → Service VLANs)');
|
||
|
|
console.log(' - Monitoring access (Service VLANs → VLAN 11)');
|
||
|
|
console.log(' - Other inter-VLAN rules as needed');
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('❌ Error:', error.message);
|
||
|
|
if (error.stack) {
|
||
|
|
console.error(error.stack);
|
||
|
|
}
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
main();
|