Skip to main content
Glama
manager.ts25 kB
import MikrotikSSH, { ExecResult } from './client.js'; import { MIKROTIK_COMMANDS } from '../../lib/mikrotik/index.js'; import * as builders from '../../lib/mikrotik/commands/builders.js'; import type { BulkDeviceTarget, BulkApplyReport, VlanWizardOptions, DhcpQuickOptions, L2tpServerOptions, IpsecSiteToSiteOptions, SimpleQueueOptions, NetwatchOptions, BackupOptions } from '../../lib/mikrotik/types.js'; export { MIKROTIK_COMMANDS }; export type BackupConfig = { name: string; compressed?: boolean; encryptionPassword?: string; }; export type ConfigSnapshot = { timestamp: number; deviceId: string; config: string; hash: string; }; export type DiffResult = { added: string[]; removed: string[]; modified: string[]; }; export type ConfigApplyResult = { success: boolean; appliedCommands: number; failedCommands: number; errors: string[]; }; /** * Centralized MikroTik configuration manager. * Handles backup, restore, diff, and configuration application. */ export class MikrotikManager { private ssh: MikrotikSSH; private deviceId: string; constructor(deviceId: string) { this.ssh = new MikrotikSSH(); this.deviceId = deviceId; } async connect(host: string, username: string, password?: string, port = 22): Promise<void> { await this.ssh.connect({ host, port, username, password }); } async disconnect(): Promise<void> { await this.ssh.disconnect(); } /** * Get system information from the device. */ async getSystemInfo(): Promise<Record<string, unknown>> { const results = await this.ssh.execMulti([ MIKROTIK_COMMANDS.SYSTEM_IDENTITY, MIKROTIK_COMMANDS.SYSTEM_RESOURCES, MIKROTIK_COMMANDS.SYSTEM_PACKAGE_PRINT, ]); return { identity: results[MIKROTIK_COMMANDS.SYSTEM_IDENTITY], resources: results[MIKROTIK_COMMANDS.SYSTEM_RESOURCES], packages: results[MIKROTIK_COMMANDS.SYSTEM_PACKAGE_PRINT], }; } /** * Create a configuration backup with optional compression and encryption. */ async createBackup(config: BackupConfig): Promise<ExecResult> { let cmd = MIKROTIK_COMMANDS.BACKUP_CREATE(config.name); // MikroTik backup command doesn't have direct compression flag in CLI, // but the binary backup is compressed by default if (config.encryptionPassword) { // Password protection would be handled through export instead cmd = MIKROTIK_COMMANDS.BACKUP_EXPORT(config.name); } return this.ssh.exec(cmd); } /** * List all backups on the device. */ async listBackups(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.BACKUP_LIST); } /** * Restore a configuration backup. */ async restoreBackup(backupName: string): Promise<ExecResult> { // Warning: This is a destructive operation console.warn(`⚠️ Restoring backup: ${backupName} will replace current configuration`); return this.ssh.exec(MIKROTIK_COMMANDS.BACKUP_RESTORE(backupName)); } /** * Export current configuration as text (for version control/diff). */ async exportConfig(filename?: string): Promise<string> { const fname = filename || `config-${Date.now()}.rsc`; const result = await this.ssh.exec(MIKROTIK_COMMANDS.BACKUP_EXPORT(fname)); if (result.exitCode !== 0) { throw new Error(`Export failed: ${result.stderr}`); } // Retrieve the exported file content const getCmd = `/file get name="${fname}" contents`; const getResult = await this.ssh.exec(getCmd); if (getResult.exitCode === 0) { return getResult.stdout; } return result.stdout; } /** * Get current configuration as ConfigSnapshot. */ async getConfigSnapshot(): Promise<ConfigSnapshot> { const config = await this.exportConfig(); const hash = this.computeHash(config); return { timestamp: Date.now(), deviceId: this.deviceId, config, hash, }; } /** * Compare two configuration snapshots and return diff. */ computeDiff(before: ConfigSnapshot, after: ConfigSnapshot): DiffResult { const beforeLines = new Set(before.config.split('\n')); const afterLines = new Set(after.config.split('\n')); const added = Array.from(afterLines).filter(line => !beforeLines.has(line)); const removed = Array.from(beforeLines).filter(line => !afterLines.has(line)); return { added, removed, modified: [], // simplified; could parse more detailed diffs }; } /** * Apply a list of commands with error handling and rollback support. */ async applyCommands( commands: string[], opts?: { rollbackOnError?: boolean; dryRun?: boolean; } ): Promise<ConfigApplyResult> { const dryRun = opts?.dryRun ?? false; const rollbackOnError = opts?.rollbackOnError ?? false; if (dryRun) { // Validate commands syntax without executing return { success: true, appliedCommands: 0, failedCommands: 0, errors: [], }; } const beforeSnapshot = await this.getConfigSnapshot(); const results = await this.ssh.execMulti(commands, { stopOnError: rollbackOnError }); const errors: string[] = []; let failedCount = 0; let successCount = 0; for (const [cmd, result] of Object.entries(results)) { if (result.exitCode !== 0) { failedCount++; errors.push(`${cmd}: ${result.stderr}`); } else { successCount++; } } // If rollback requested and errors occurred if (rollbackOnError && failedCount > 0) { console.warn('Rolling back due to errors...'); await this.restoreBackup(`backup-before-apply-${beforeSnapshot.timestamp}`); } return { success: failedCount === 0, appliedCommands: successCount, failedCommands: failedCount, errors, }; } /** * Simple hash function for config snapshots (use crypto.createHash in production). */ private computeHash(content: string): string { let hash = 0; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(16); } /** * Validate command syntax before applying. */ validateCommand(cmd: string): { valid: boolean; error?: string } { // Basic validation: MikroTik commands start with / if (!cmd.trim().startsWith('/')) { return { valid: false, error: 'Command must start with /' }; } // Check for dangerous commands without confirmation const dangerousPatterns = [ '/system reboot', '/system shutdown', '/user remove', '/ip firewall nat reset', ]; for (const pattern of dangerousPatterns) { if (cmd.includes(pattern)) { return { valid: false, error: `Dangerous command: ${pattern}. Requires explicit confirmation.` }; } } return { valid: true }; } /** * Get all IP addresses configured on the device. */ async getIPAddresses(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.IP_ADDRESS_LIST); } /** * Get all routes configured on the device. */ async getRoutes(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.IP_ROUTE_LIST); } /** * Get all interfaces on the device. */ async getInterfaces(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.INTERFACE_LIST); } async getBridges(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.BRIDGE_LIST); } async getBridgePorts(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.BRIDGE_PORT_LIST); } async getBridgeVlans(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.BRIDGE_VLAN_LIST); } /** * List all DHCP servers. */ async getDHCPServers(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.DHCP_SERVER_LIST); } /** * List all DHCP pools. */ async getDHCPPools(): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.DHCP_POOL_LIST); } /** * Setup DHCP server on an interface. * Parameters: interface name, pool name, address space (e.g., "192.168.1.0/24") */ async setupDHCP( interfaceName: string, poolName: string, addressSpace: string, rangeStart: string, rangeEnd: string, gateway: string, dnsServers: string[] = ['8.8.8.8'] ): Promise<ConfigApplyResult> { const commands = [ MIKROTIK_COMMANDS.DHCP_POOL_ADD(poolName, rangeStart, rangeEnd), MIKROTIK_COMMANDS.DHCP_SERVER_ADD(interfaceName, poolName), MIKROTIK_COMMANDS.DHCP_NETWORK_ADD(addressSpace, gateway, dnsServers), ]; return this.applyCommands(commands); } /** * Monitor system logs. */ async getLogs(count = 100): Promise<ExecResult> { return this.ssh.exec(`/log print follow=no numbers=0..${count}`); } /** * Create a bridge and optionally enable vlan-filtering. */ async createBridge(name: string, opts?: { vlanFiltering?: boolean }): Promise<ConfigApplyResult> { const commands = [ MIKROTIK_COMMANDS.INTERFACE_BRIDGE_ADD(name), ]; if (opts?.vlanFiltering !== undefined) { commands.push(MIKROTIK_COMMANDS.BRIDGE_SET_VLAN_FILTERING(name, opts.vlanFiltering)); } return this.applyCommands(commands); } /** * Add port to bridge with optional PVID and frame type. */ async addBridgePort( bridge: string, iface: string, opts?: { pvid?: number; frameTypes?: 'admit-all' | 'admit-only-untagged-and-priority-tagged' | 'admit-only-vlan-tagged' } ): Promise<ConfigApplyResult> { const commands = [ MIKROTIK_COMMANDS.INTERFACE_BRIDGE_PORT_ADD(bridge, iface), ]; if (opts?.pvid !== undefined) { commands.push(MIKROTIK_COMMANDS.BRIDGE_PORT_SET_PVID(iface, opts.pvid)); } if (opts?.frameTypes) { commands.push(MIKROTIK_COMMANDS.BRIDGE_PORT_SET_FRAME_TYPES(iface, opts.frameTypes)); } return this.applyCommands(commands); } /** * Configure bridge VLANs (tagged/untagged) and optionally enable vlan-filtering. */ async configureBridgeVlan( bridge: string, vlanId: number, tagged: string[], untagged: string[] = [], opts?: { enableFiltering?: boolean } ): Promise<ConfigApplyResult> { const commands = [] as string[]; if (opts?.enableFiltering) { commands.push(MIKROTIK_COMMANDS.BRIDGE_SET_VLAN_FILTERING(bridge, true)); } commands.push(MIKROTIK_COMMANDS.BRIDGE_VLAN_ADD(bridge, vlanId, tagged, untagged)); return this.applyCommands(commands); } /** * Set interface administrative state. */ async setInterfaceState(iface: string, enabled: boolean): Promise<ExecResult> { return this.ssh.exec(enabled ? MIKROTIK_COMMANDS.INTERFACE_ENABLE(iface) : MIKROTIK_COMMANDS.INTERFACE_DISABLE(iface)); } /** * Set interface MTU. */ async setInterfaceMtu(iface: string, mtu: number): Promise<ExecResult> { return this.ssh.exec(MIKROTIK_COMMANDS.INTERFACE_SET_MTU(iface, mtu)); } /** * Create bonding interface. */ async createBonding(name: string, slaves: string[], mode: string = '802.3ad'): Promise<ConfigApplyResult> { return this.applyCommands([MIKROTIK_COMMANDS.INTERFACE_BONDING_ADD(name, slaves, mode)]); } /** * Set port as access mode (untagged) on given bridge and VLAN. */ async setAccessPort(bridge: string, iface: string, vlanId: number): Promise<ConfigApplyResult> { const commands = [ MIKROTIK_COMMANDS.INTERFACE_BRIDGE_PORT_ADD(bridge, iface), MIKROTIK_COMMANDS.BRIDGE_PORT_SET_PVID(iface, vlanId), MIKROTIK_COMMANDS.BRIDGE_VLAN_ADD(bridge, vlanId, [], [iface]), ]; return this.applyCommands(commands); } /** * Set port as trunk mode with tagged VLANs on a bridge. */ async setTrunkPort(bridge: string, iface: string, vlanIds: number[]): Promise<ConfigApplyResult> { const commands = [ MIKROTIK_COMMANDS.INTERFACE_BRIDGE_PORT_ADD(bridge, iface), MIKROTIK_COMMANDS.BRIDGE_PORT_SET_FRAME_TYPES(iface, 'admit-only-vlan-tagged'), ...vlanIds.map((vlanId) => MIKROTIK_COMMANDS.BRIDGE_VLAN_ADD(bridge, vlanId, [iface], [])), ]; return this.applyCommands(commands); } /** * VLAN wizard: create VLAN interface, add bridge-vlan, set IP, DHCP pool and dhcp-network */ async createVlanNetwork(opts: VlanWizardOptions): Promise<ConfigApplyResult> { try { const dhcpPool = opts.dhcpPool || `${opts.poolStart}-${opts.poolEnd}`; const commands = builders.buildVlanNetwork({ ...opts, dhcpPool }); return this.applyCommands(commands); } catch (err) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: [err instanceof Error ? err.message : String(err)], }; } } /** * Apply a basic firewall template: block inbound from WAN, allow established, masquerade */ async applyFirewallTemplate(wanIface: string, lanAddressOrBridge: string): Promise<ConfigApplyResult> { if (!wanIface || !lanAddressOrBridge) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const commands = builders.buildFirewallTemplate(wanIface, lanAddressOrBridge); return this.applyCommands(commands); } /** * Add addresses to address-list and add a block rule */ async addBlockAddressList(listName: string, addressesCsv: string, action: 'drop' | 'reject' = 'drop'): Promise<ConfigApplyResult> { if (!listName || !addressesCsv) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const commands = builders.buildBlockAddressList(listName, addressesCsv, action); return this.applyCommands(commands); } /** * Force DNS via dst-nat for in-LAN traffic and set router DNS */ async enforceDns(lanInterface: string, primary: string, secondary?: string): Promise<ConfigApplyResult> { if (!lanInterface || !primary) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const commands = builders.buildDnsForce(lanInterface, primary, secondary); return this.applyCommands(commands); } /** * Quick DHCP setup: add pool, address, dhcp-server and network */ async setupDhcpQuick(opts: DhcpQuickOptions): Promise<ConfigApplyResult> { try { const commands = builders.buildDhcpQuick(opts); return this.applyCommands(commands); } catch (err) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: [err instanceof Error ? err.message : String(err)], }; } } /** * Set timezone and NTP servers */ async setTimeNtp(timezone: string, ntpPrimary: string, ntpSecondary?: string): Promise<ConfigApplyResult> { if (!timezone || !ntpPrimary) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const commands = builders.buildTimeNtp(timezone, ntpPrimary, ntpSecondary); return this.applyCommands(commands); } /** * Configure identity and SNMP */ async configureIdentitySnmp(identity: string, location: string | undefined, contact: string | undefined, community: string, trapTarget?: string): Promise<ConfigApplyResult> { if (!identity || !community) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const commands = builders.buildIdentitySnmp(identity, location || 'datacenter', contact || 'noc', community, trapTarget); return this.applyCommands(commands); } /** * Configure L2TP server (basic) with ipsec */ async setupL2tpServer(opts: { publicIp: string; poolRange: string; profileName: string; dns: string[]; secret: string; user: string; password: string }): Promise<ConfigApplyResult> { const { publicIp, poolRange, profileName, dns, secret, user, password } = opts; if (!publicIp || !poolRange || !profileName || !dns?.length || !secret || !user || !password) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const l2tpOpts: L2tpServerOptions = { profile: profileName, localAddress: publicIp, remoteAddressPool: poolRange, secret, username: user, }; const commands = builders.buildL2tpServer(l2tpOpts); return this.applyCommands(commands); } /** * Configure an IPsec site-to-site peer and policy (basic) */ async setupIpsecSiteToSite(opts: { peerName: string; peerIp: string; localId?: string; localSubnet: string; remoteSubnet: string; preSharedKey: string }): Promise<ConfigApplyResult> { const { peerName, peerIp, localSubnet, remoteSubnet, preSharedKey } = opts; if (!peerName || !peerIp || !localSubnet || !remoteSubnet || !preSharedKey) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const ipsecOpts: IpsecSiteToSiteOptions = { peerAddress: peerIp, psk: preSharedKey, localSubnet, remoteSubnet, }; const commands = builders.buildIpsecSiteToSite(ipsecOpts); return this.applyCommands(commands); } /** * Configure remote syslog */ async configureSyslog(remoteIp: string, port = 514, topics = 'info'): Promise<ConfigApplyResult> { if (!remoteIp) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const commands = builders.buildSyslogRemote(remoteIp, port, topics); return this.applyCommands(commands); } /** * Add a netwatch rule */ async addNetwatch(host: string, interval = '00:00:30', downScript?: string, upScript?: string): Promise<ConfigApplyResult> { if (!host) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing host'] }; } const opts: NetwatchOptions = { host, interval, downScript, upScript }; const commands = builders.buildNetwatch(opts); return this.applyCommands(commands); } /** * Create a backup and optionally upload via scp */ async createBackupRemote(filename: string, scpUser?: string, scpHost?: string, scpPath?: string): Promise<ConfigApplyResult> { if (!filename) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing filename'] }; } const uploadTo = scpUser && scpHost && scpPath ? `scp://${scpUser}@${scpHost}${scpPath}` : undefined; const opts: BackupOptions = { name: filename, uploadTo }; const commands = builders.buildBackup(opts); return this.applyCommands(commands); } /** * Add simple queue for single IP */ async addSimpleQueueIp(name: string, targetIp: string, maxUp: string, maxDown: string, _limitAtUp?: string, _limitAtDown?: string): Promise<ConfigApplyResult> { if (!name || !targetIp || !maxUp || !maxDown) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const opts: SimpleQueueOptions = { name, target: targetIp, maxUpload: maxUp, maxDownload: maxDown }; const commands = builders.buildSimpleQueue(opts); return this.applyCommands(commands); } /** * Add simple queue for subnet */ async addSimpleQueueSubnet(name: string, subnet: string, maxUp: string, maxDown: string): Promise<ConfigApplyResult> { if (!name || !subnet || !maxUp || !maxDown) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['missing parameters'] }; } const opts: SimpleQueueOptions = { name, target: subnet, maxUpload: maxUp, maxDownload: maxDown }; const commands = builders.buildSimpleQueue(opts); return this.applyCommands(commands); } /** * Run ping and traceroute via ssh return ExecResult map */ async runToolkit(target: string, count = 4, size = 64): Promise<Record<string, ExecResult>> { const commands = builders.buildToolkit(target, count, size); const ping = await this.ssh.exec(commands[0]); const tracer = await this.ssh.exec(commands[1]); return { ping, tracer } as Record<string, ExecResult>; } /** * Run torch on device (returns ExecResult) */ async runTorch(interfaceName: string, port?: string, ip?: string): Promise<ExecResult> { if (!interfaceName) throw new Error('interface required'); const params = [`interface=${interfaceName}`]; if (port) params.push(`port=${port}`); if (ip) params.push(`ip-address=${ip}`); return this.ssh.exec(`/tool torch ${params.join(' ')}`); } /** * Disable selected services */ async disableServices(opts: { www?: boolean; telnet?: boolean; ssh?: boolean; ftp?: boolean; api?: boolean; apiSsl?: boolean }): Promise<ConfigApplyResult> { if (!Object.values(opts).some(v => v)) { return { success: false, appliedCommands: 0, failedCommands: 0, errors: ['no services selected'] }; } const normalizedOpts = { ...opts, apissl: opts.apiSsl }; const commands = builders.buildDisableServices(normalizedOpts); return this.applyCommands(commands); } /** * Install brute-force protection firewall rules for SSH/Winbox */ async enableBruteForceProtection(sshPort = 22, winboxPort = 8291, threshold = 5, blockTimeout = '1h'): Promise<ConfigApplyResult> { const commands = builders.buildBruteForceProtection(sshPort, winboxPort, threshold, blockTimeout); return this.applyCommands(commands); } } /** * Apply the same command set to multiple MikroTik devices sequentially. */ export async function applyCommandsBulk( targets: BulkDeviceTarget[], commands: string[], options?: { rollbackOnError?: boolean; dryRun?: boolean } ): Promise<BulkApplyReport[]> { const reports: BulkApplyReport[] = []; if (!targets.length || !commands.length) return reports; for (const target of targets) { const manager = new MikrotikManager(target.deviceId || target.host); try { await manager.connect(target.host, target.username, target.password, target.port ?? 22); const result = await manager.applyCommands(commands, { rollbackOnError: options?.rollbackOnError, dryRun: options?.dryRun, }); reports.push({ device: target.host, success: result.success, result }); } catch (err) { reports.push({ device: target.host, success: false, error: err instanceof Error ? err.message : String(err), }); } finally { try { await manager.disconnect(); } catch { // ignore disconnect errors to continue other devices } } } return reports; } export default MikrotikManager;

Latest Blog Posts

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/babasida246/ai-mcp-gateway'

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