Skip to main content
Glama

OPNSense MCP Server

nat.ts28.6 kB
// NAT Rule Resource Implementation for OPNsense // Uses SSH/CLI for NAT management since REST API doesn't support NAT operations import { OPNSenseAPIClient } from '../../api/client.js'; import { InterfaceMapper } from '../../utils/interface-mapper.js'; import SSHExecutor from '../ssh/executor.js'; import { parseStringPromise, Builder } from 'xml2js'; // NAT Rule Types export interface OutboundNATRule { uuid?: string; enabled?: string; // '0' or '1' sequence?: string; // Rule order/priority interface?: string; // Outbound interface (e.g., 'wan') source?: any; // Source specification object source_net?: string; // Source network (simplified) source_port?: string; // Source port destination?: any; // Destination specification object destination_net?: string; // Destination network (simplified) destination_port?: string; // Destination port target?: string; // NAT target (IP or alias, empty for no NAT) targetip?: string; // Target IP address targetip_subnet?: string; // Target subnet mask poolopts?: string; // Pool options sourceHash?: string; // Source hash for sticky connections nonat?: string; // '1' for no NAT (exception rule) log?: string; // '0' or '1' description?: string; // Rule description created?: any; // Creation metadata updated?: any; // Update metadata } export interface PortForwardRule { uuid?: string; enabled?: string; // '0' or '1' interface?: string; // Inbound interface protocol?: string; // 'tcp', 'udp', 'tcp/udp' source?: any; // Source specification source_net?: string; // Source network source_port?: string; // Source port destination?: any; // Destination specification destination_net?: string; // External destination destination_port?: string; // External port target?: string; // Internal target IP local_port?: string; // Internal port description?: string; // Rule description associated_rule?: string; // Associated filter rule nosync?: string; // Disable state sync log?: string; // Enable logging } export interface OneToOneNATRule { uuid?: string; enabled?: string; // '0' or '1' interface?: string; // Interface external?: string; // External IP address internal?: string; // Internal IP address description?: string; // Rule description } export interface NPTRule { uuid?: string; enabled?: string; // '0' or '1' interface?: string; // Interface source?: string; // Source IPv6 prefix destination?: string; // Destination IPv6 prefix description?: string; // Rule description } export type NATMode = 'automatic' | 'hybrid' | 'manual' | 'disabled'; export class NATResource { private client: OPNSenseAPIClient; private interfaceMapper: InterfaceMapper; private sshExecutor: SSHExecutor | null = null; private debugMode: boolean = process.env.MCP_DEBUG === 'true' || process.env.DEBUG_NAT === 'true'; private useSSH: boolean = false; constructor(client: OPNSenseAPIClient) { this.client = client; this.interfaceMapper = new InterfaceMapper(); // Check if SSH is configured if (process.env.OPNSENSE_SSH_HOST && process.env.OPNSENSE_SSH_USERNAME) { this.sshExecutor = new SSHExecutor(); this.useSSH = true; if (this.debugMode) { console.log('[NATResource] SSH configured, using SSH/CLI for NAT management'); } } else { if (this.debugMode) { console.log('[NATResource] SSH not configured, limited NAT functionality available'); } } } // ==================== SSH/XML-based NAT Methods ==================== /** * Get NAT configuration from config.xml via SSH */ private async getNATConfigViaSSH(): Promise<any> { if (!this.sshExecutor) { throw new Error('SSH not configured. Please set OPNSENSE_SSH_HOST, OPNSENSE_SSH_USERNAME, and OPNSENSE_SSH_PASSWORD in environment variables.'); } const result = await this.sshExecutor.execute('cat /conf/config.xml'); if (!result.success) { throw new Error(`Failed to read config.xml: ${result.stderr}`); } try { const config = await parseStringPromise(result.stdout, { explicitArray: false, ignoreAttrs: true }); return config.opnsense || config; } catch (error) { throw new Error(`Failed to parse config.xml: ${error}`); } } /** * Save NAT configuration to config.xml via SSH */ private async saveNATConfigViaSSH(config: any): Promise<void> { if (!this.sshExecutor) { throw new Error('SSH not configured'); } // Convert config back to XML const builder = new Builder({ xmldec: { version: '1.0', encoding: 'UTF-8' }, renderOpts: { pretty: true, indent: ' ' } }); const xml = builder.buildObject({ opnsense: config }); // Save to temporary file and then move to config.xml const tempFile = `/tmp/config_nat_${Date.now()}.xml`; const commands = [ // Backup current config 'cp /conf/config.xml /conf/config.xml.backup', // Write new config to temp file `cat > ${tempFile} << 'EOF'\n${xml}\nEOF`, // Validate XML structure `xmllint --noout ${tempFile}`, // Replace config if valid `mv ${tempFile} /conf/config.xml`, // Reload configuration 'configctl firmware reload', 'configctl filter reload' ]; const batch = await this.sshExecutor.executeBatch(commands, { stopOnError: true }); if (!batch.success) { throw new Error(`Failed to save NAT configuration: ${batch.results.map(r => r.stderr).join('\n')}`); } } /** * Generate UUID for new rules */ private generateUUID(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } // ==================== Outbound NAT Methods (SSH Implementation) ==================== /** * List all outbound NAT rules (SSH implementation) */ async listOutboundRules(): Promise<OutboundNATRule[]> { if (this.debugMode) { console.log('[NATResource] Fetching outbound NAT rules via SSH'); } if (!this.sshExecutor) { console.warn('[NATResource] SSH not configured, returning empty list'); return []; } try { const config = await this.getNATConfigViaSSH(); const natConfig = config.nat || {}; const outbound = natConfig.outbound || {}; if (this.debugMode) { console.log('[NATResource] Outbound NAT mode:', outbound.mode); } // Handle rules - they might be an array or object let rules: OutboundNATRule[] = []; if (outbound.rule) { if (Array.isArray(outbound.rule)) { rules = outbound.rule.map((rule: any) => this.normalizeOutboundRuleFromXML(rule)); } else if (typeof outbound.rule === 'object') { // Single rule as object rules = [this.normalizeOutboundRuleFromXML(outbound.rule)]; } } if (this.debugMode) { console.log(`[NATResource] Found ${rules.length} outbound NAT rules`); } return rules; } catch (error) { console.error('Failed to list outbound NAT rules:', error); return []; } } /** * Get current outbound NAT mode (SSH implementation) */ async getOutboundMode(): Promise<NATMode> { if (!this.sshExecutor) { console.warn('[NATResource] SSH not configured, returning default mode'); return 'automatic'; } try { const config = await this.getNATConfigViaSSH(); const mode = config.nat?.outbound?.mode || 'automatic'; return mode as NATMode; } catch (error) { console.error('Failed to get outbound NAT mode:', error); return 'automatic'; } } /** * Set outbound NAT mode (SSH implementation) */ async setOutboundMode(mode: NATMode): Promise<boolean> { if (this.debugMode) { console.log(`[NATResource] Setting outbound NAT mode to: ${mode}`); } if (!this.sshExecutor) { throw new Error('SSH not configured. Cannot modify NAT mode.'); } try { const config = await this.getNATConfigViaSSH(); // Ensure NAT structure exists if (!config.nat) config.nat = {}; if (!config.nat.outbound) config.nat.outbound = {}; // Set the mode config.nat.outbound.mode = mode; // Save configuration await this.saveNATConfigViaSSH(config); return true; } catch (error) { console.error('Failed to set outbound NAT mode:', error); return false; } } /** * Create an outbound NAT rule (SSH implementation) */ async createOutboundRule(rule: OutboundNATRule): Promise<{ uuid: string; success: boolean }> { if (this.debugMode) { console.log('[NATResource] Creating outbound NAT rule via SSH:', rule); } if (!this.sshExecutor) { throw new Error('SSH not configured. Cannot create NAT rules.'); } try { const config = await this.getNATConfigViaSSH(); // Ensure NAT structure exists if (!config.nat) config.nat = {}; if (!config.nat.outbound) config.nat.outbound = {}; if (!config.nat.outbound.rule) config.nat.outbound.rule = []; // Generate UUID for the new rule const uuid = this.generateUUID(); // Build the rule XML structure const xmlRule: any = { interface: rule.interface || 'wan', source: { network: rule.source_net || 'any' }, destination: rule.destination_net ? { network: rule.destination_net } : { any: '' }, descr: rule.description || '' }; // Handle no-NAT rules if (rule.nonat === '1' || rule.target === '') { xmlRule.nonat = ''; } else if (rule.target) { xmlRule.target = rule.target; } // Add optional fields if (rule.enabled !== undefined) xmlRule.disabled = rule.enabled === '0' ? '' : undefined; if (rule.source_port) xmlRule.sourceport = rule.source_port; if (rule.destination_port) xmlRule.dstport = rule.destination_port; if (rule.poolopts) xmlRule.poolopts = rule.poolopts; if (rule.log === '1') xmlRule.log = ''; // Ensure rule array is an array if (!Array.isArray(config.nat.outbound.rule)) { config.nat.outbound.rule = config.nat.outbound.rule ? [config.nat.outbound.rule] : []; } // Add the rule config.nat.outbound.rule.push(xmlRule); // Save configuration await this.saveNATConfigViaSSH(config); // Apply changes await this.applyNATChanges(); return { uuid, success: true }; } catch (error) { console.error('Failed to create outbound NAT rule:', error); throw error; } } /** * Delete an outbound NAT rule (SSH implementation) */ async deleteOutboundRule(description: string): Promise<boolean> { if (!this.sshExecutor) { throw new Error('SSH not configured. Cannot delete NAT rules.'); } try { const config = await this.getNATConfigViaSSH(); if (!config.nat?.outbound?.rule) { throw new Error('No outbound NAT rules found'); } // Find and remove the rule by description let rules = config.nat.outbound.rule; if (!Array.isArray(rules)) { rules = [rules]; } const filteredRules = rules.filter((r: any) => r.descr !== description); if (filteredRules.length === rules.length) { throw new Error(`Rule with description "${description}" not found`); } config.nat.outbound.rule = filteredRules; // Save configuration await this.saveNATConfigViaSSH(config); // Apply changes await this.applyNATChanges(); return true; } catch (error) { console.error('Failed to delete outbound NAT rule:', error); throw error; } } /** * Update an outbound NAT rule (placeholder for SSH implementation) */ async updateOutboundRule(uuid: string, updates: Partial<OutboundNATRule>): Promise<boolean> { // For SSH implementation, we would need to identify rules by description or other fields console.warn('[NATResource] Update via SSH not fully implemented. Use delete and create instead.'); return false; } /** * Toggle outbound rule enabled/disabled (placeholder for SSH implementation) */ async toggleOutboundRule(uuid: string, enabled?: string): Promise<boolean> { console.warn('[NATResource] Toggle via SSH not fully implemented'); return false; } /** * Get a specific outbound NAT rule (placeholder for SSH implementation) */ async getOutboundRule(uuid: string): Promise<OutboundNATRule | null> { console.warn('[NATResource] Get specific rule via SSH not implemented'); return null; } // ==================== Port Forwarding Methods (Placeholders) ==================== async listPortForwards(): Promise<PortForwardRule[]> { if (this.debugMode) { console.log('[NATResource] Port forwarding via SSH not yet implemented'); } return []; } async createPortForward(rule: PortForwardRule): Promise<{ uuid: string; success: boolean }> { throw new Error('Port forwarding via SSH not yet implemented'); } async deletePortForward(uuid: string): Promise<boolean> { throw new Error('Port forwarding via SSH not yet implemented'); } // ==================== One-to-One NAT Methods (Placeholders) ==================== async listOneToOneRules(): Promise<OneToOneNATRule[]> { return []; } async createOneToOneRule(rule: OneToOneNATRule): Promise<{ uuid: string; success: boolean }> { throw new Error('One-to-One NAT via SSH not yet implemented'); } // ==================== NPT Methods (Placeholders) ==================== async listNPTRules(): Promise<NPTRule[]> { return []; } // ==================== Critical Fix Methods ==================== /** * Fix DMZ NAT issue - Add exception rules for inter-VLAN traffic */ async fixDMZNAT(params?: { dmzNetwork?: string; lanNetwork?: string; otherInternalNetworks?: string[]; }): Promise<{ success: boolean; message: string; rulesCreated: string[] }> { console.log('\n[NATResource] Starting DMZ NAT fix...'); const dmzNetwork = params?.dmzNetwork || '10.0.6.0/24'; const lanNetwork = params?.lanNetwork || '10.0.0.0/24'; const otherNetworks = params?.otherInternalNetworks || [ '10.0.2.0/24', // IoT VLAN '10.0.4.0/24' // Guest VLAN ]; const rulesCreated: string[] = []; try { // Step 1: Check current NAT mode const currentMode = await this.getOutboundMode(); console.log(`Current NAT mode: ${currentMode}`); // Step 2: Set to hybrid mode if needed (allows manual rules with automatic) if (currentMode === 'automatic') { console.log('Switching NAT mode from automatic to hybrid...'); await this.setOutboundMode('hybrid'); } // Step 3: Create no-NAT exception rules for inter-VLAN traffic // DMZ to LAN - No NAT console.log('Creating DMZ to LAN no-NAT rule...'); const dmzToLan = await this.createOutboundRule({ enabled: '1', interface: 'wan', source_net: dmzNetwork, destination_net: lanNetwork, nonat: '1', target: '', description: 'No NAT: DMZ to LAN traffic (MCP auto-fix)' }); rulesCreated.push(`DMZ→LAN`); // LAN to DMZ - No NAT (return traffic) console.log('Creating LAN to DMZ no-NAT rule...'); const lanToDmz = await this.createOutboundRule({ enabled: '1', interface: 'wan', source_net: lanNetwork, destination_net: dmzNetwork, nonat: '1', target: '', description: 'No NAT: LAN to DMZ traffic (MCP auto-fix)' }); rulesCreated.push(`LAN→DMZ`); // DMZ to other internal networks for (const network of otherNetworks) { console.log(`Creating DMZ to ${network} no-NAT rule...`); await this.createOutboundRule({ enabled: '1', interface: 'wan', source_net: dmzNetwork, destination_net: network, nonat: '1', target: '', description: `No NAT: DMZ to ${network} traffic (MCP auto-fix)` }); rulesCreated.push(`DMZ→${network}`); // Reverse direction await this.createOutboundRule({ enabled: '1', interface: 'wan', source_net: network, destination_net: dmzNetwork, nonat: '1', target: '', description: `No NAT: ${network} to DMZ traffic (MCP auto-fix)` }); rulesCreated.push(`${network}→DMZ`); } // Step 4: Apply all NAT changes console.log('Applying NAT configuration...'); await this.applyNATChanges(); // Step 5: Verify the rules were created const rules = await this.listOutboundRules(); const mcpRules = rules.filter(r => r.description?.includes('MCP auto-fix')); console.log(`\n✅ DMZ NAT fix completed successfully!`); console.log(`Created ${rulesCreated.length} no-NAT exception rules`); console.log(`Verified ${mcpRules.length} MCP rules in configuration`); return { success: true, message: `Successfully fixed DMZ NAT issue. Created ${rulesCreated.length} no-NAT exception rules for inter-VLAN traffic.`, rulesCreated }; } catch (error) { console.error('Failed to fix DMZ NAT:', error); return { success: false, message: `Failed to fix DMZ NAT: ${error instanceof Error ? error.message : 'Unknown error'}`, rulesCreated }; } } /** * Quick fix for DMZ NAT issue with minimal configuration */ async quickFixDMZNAT(): Promise<{ success: boolean; message: string }> { console.log('\n[NATResource] Quick DMZ NAT fix...'); try { // Set to hybrid mode await this.setOutboundMode('hybrid'); // Create essential no-NAT rules const rules = [ { src: '10.0.6.0/24', dst: '10.0.0.0/24', desc: 'DMZ to LAN' }, { src: '10.0.0.0/24', dst: '10.0.6.0/24', desc: 'LAN to DMZ' }, { src: '10.0.6.0/24', dst: '10.0.2.0/24', desc: 'DMZ to IoT' }, { src: '10.0.2.0/24', dst: '10.0.6.0/24', desc: 'IoT to DMZ' } ]; for (const rule of rules) { await this.createOutboundRule({ enabled: '1', interface: 'wan', source_net: rule.src, destination_net: rule.dst, nonat: '1', target: '', description: `No NAT: ${rule.desc} (Quick fix)` }); } await this.applyNATChanges(); return { success: true, message: 'DMZ NAT issue fixed! Inter-VLAN traffic will no longer be NAT\'d.' }; } catch (error) { return { success: false, message: `Quick fix failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Remove all MCP-created NAT fix rules */ async cleanupDMZNATFix(): Promise<{ success: boolean; deletedCount: number }> { console.log('\n[NATResource] Cleaning up MCP NAT fix rules...'); try { const rules = await this.listOutboundRules(); const mcpRules = rules.filter(r => r.description?.includes('MCP auto-fix') || r.description?.includes('Quick fix') ); let deletedCount = 0; for (const rule of mcpRules) { if (rule.description) { console.log(`Deleting rule: ${rule.description}`); await this.deleteOutboundRule(rule.description); deletedCount++; } } if (deletedCount > 0) { await this.applyNATChanges(); } return { success: true, deletedCount }; } catch (error) { console.error('Cleanup failed:', error); return { success: false, deletedCount: 0 }; } } // ==================== Diagnostic Methods ==================== /** * Analyze NAT configuration for issues */ async analyzeNATConfiguration(): Promise<{ mode: NATMode; issues: string[]; recommendations: string[]; rules: { outbound: number; portForwards: number; oneToOne: number; npt: number; }; }> { console.log('\n[NATResource] Analyzing NAT configuration...'); const issues: string[] = []; const recommendations: string[] = []; // Get current configuration const mode = await this.getOutboundMode(); const outboundRules = await this.listOutboundRules(); const portForwards = await this.listPortForwards(); const oneToOneRules = await this.listOneToOneRules(); const nptRules = await this.listNPTRules(); // Check for common issues if (mode === 'automatic') { // Check for internal networks being NAT'd const internalNetworks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']; const hasInternalNAT = outboundRules.some(r => !r.nonat && internalNetworks.some(net => r.destination_net?.includes(net.split('/')[0]) ) ); if (hasInternalNAT) { issues.push('Internal traffic may be incorrectly NAT\'d in automatic mode'); recommendations.push('Switch to hybrid mode and add no-NAT rules for internal traffic'); } } // Check for DMZ NAT issues const dmzRules = outboundRules.filter(r => r.source_net?.includes('10.0.6') || r.description?.toLowerCase().includes('dmz') ); const hasDMZExceptions = dmzRules.some(r => r.nonat === '1'); if (!hasDMZExceptions && dmzRules.length > 0) { issues.push('DMZ traffic may be NAT\'d when communicating with internal networks'); recommendations.push('Add no-NAT exception rules for DMZ to internal network traffic'); } // Check for conflicting rules const enabledOutbound = outboundRules.filter(r => r.enabled === '1'); if (enabledOutbound.length > 20) { recommendations.push('Consider consolidating NAT rules for better performance'); } // Check if SSH is configured if (!this.sshExecutor) { issues.push('SSH not configured - NAT management is limited'); recommendations.push('Configure SSH credentials in environment variables for full NAT management capabilities'); } return { mode, issues, recommendations, rules: { outbound: outboundRules.length, portForwards: portForwards.length, oneToOne: oneToOneRules.length, npt: nptRules.length } }; } // ==================== Helper Methods ==================== /** * Apply NAT configuration changes */ async applyNATChanges(): Promise<any> { if (this.debugMode) { console.log('[NATResource] Applying NAT changes...'); } if (this.sshExecutor) { // Apply via SSH const result = await this.sshExecutor.execute('configctl filter reload'); if (!result.success) { throw new Error(`Failed to apply NAT changes: ${result.stderr}`); } return result; } else { // Try API (may not work) try { const response = await this.client.post('/firewall/nat/apply'); return response; } catch (error) { console.error('Failed to apply NAT changes via API:', error); throw new Error('Cannot apply NAT changes without SSH access'); } } } /** * Normalize outbound NAT rule data from XML */ private normalizeOutboundRuleFromXML(rule: any): OutboundNATRule { return { enabled: rule.disabled ? '0' : '1', sequence: rule.sequence, interface: rule.interface, source_net: rule.source?.network || 'any', source_port: rule.sourceport, destination_net: rule.destination?.network || 'any', destination_port: rule.dstport, target: rule.target || '', targetip: rule.targetip, targetip_subnet: rule.targetip_subnet, poolopts: rule.poolopts, sourceHash: rule.sourcehash, nonat: rule.nonat !== undefined ? '1' : '0', log: rule.log !== undefined ? '1' : '0', description: rule.descr || '' }; } /** * Normalize outbound NAT rule data from API */ private normalizeOutboundRule(uuid: string, rule: any): OutboundNATRule { return { uuid, enabled: rule.enabled || '0', sequence: rule.sequence, interface: this.extractSelectedValue(rule.interface) || rule.interface, source_net: this.extractNetworkValue(rule.source), source_port: rule.source_port || rule.sourceport, destination_net: this.extractNetworkValue(rule.destination), destination_port: rule.destination_port || rule.dstport, target: rule.target || '', targetip: rule.targetip, targetip_subnet: rule.targetip_subnet, poolopts: rule.poolopts, sourceHash: rule.sourcehash || rule.sourceHash, nonat: rule.nonat || (rule.target === '' ? '1' : '0'), log: rule.log || '0', description: rule.description || '' }; } /** * Extract port forward data */ private extractPortForwardData(rule: any): Partial<PortForwardRule> { return { enabled: rule.enabled || '0', interface: this.extractSelectedValue(rule.interface) || rule.interface, protocol: this.extractSelectedValue(rule.protocol) || rule.protocol, source_net: this.extractNetworkValue(rule.source), source_port: rule.source?.port || rule.source_port, destination_net: this.extractNetworkValue(rule.destination), destination_port: rule.destination?.port || rule.destination_port, target: rule.target, local_port: rule.local_port || rule.localport, description: rule.description || '', associated_rule: rule.associated_rule, log: rule.log || '0' }; } /** * Extract network value from complex object */ private extractNetworkValue(obj: any): string { if (!obj || typeof obj !== 'object') { return obj || 'any'; } if (obj.any === '1' || obj.any === true || obj.any === '') { return 'any'; } return obj.network || obj.address || 'any'; } /** * Extract selected value from option object */ private extractSelectedValue(optionObj: any): string { if (!optionObj || typeof optionObj !== 'object') { return optionObj || ''; } for (const [key, value] of Object.entries(optionObj)) { if (value && typeof value === 'object' && (value as any).selected === 1) { return key; } } return ''; } /** * Create common NAT presets */ createPreset(preset: string, params: any = {}): Partial<OutboundNATRule | PortForwardRule> { switch (preset) { case 'web-server': return { enabled: '1', interface: params.interface || 'wan', protocol: 'tcp', destination_port: '80,443', target: params.target || '', local_port: '80,443', description: params.description || 'Web server port forward' } as PortForwardRule; case 'ssh-forward': return { enabled: '1', interface: params.interface || 'wan', protocol: 'tcp', destination_port: params.externalPort || '2222', target: params.target || '', local_port: '22', description: params.description || 'SSH port forward' } as PortForwardRule; case 'no-nat-internal': return { enabled: '1', interface: 'wan', source_net: params.source || '10.0.0.0/8', destination_net: params.destination || '10.0.0.0/8', nonat: '1', target: '', description: params.description || 'No NAT for internal traffic' } as OutboundNATRule; default: throw new Error(`Unknown NAT preset: ${preset}`); } } } export default NATResource;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vespo92/OPNSenseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server