= 5) { $protocols[] = [ 'name' => $parts[0], 'proto' => $parts[1], 'table' => $parts[2], 'state' => $parts[3], 'since' => $parts[4], 'info' => $parts[5] ?? '', ]; } } return $protocols; } /** * Parse BIRD route count output * * @param string $output * @return array */ public static function parseRouteCount(string $output): array { $counts = [ 'total' => 0, 'primary' => 0, 'filtered' => 0, ]; // Format: X of Y routes for Z networks if (preg_match('/(\d+)\s+of\s+(\d+)\s+routes\s+for\s+(\d+)\s+networks/', $output, $matches)) { $counts['primary'] = (int) $matches[1]; $counts['total'] = (int) $matches[2]; $counts['networks'] = (int) $matches[3]; } // Filtered routes if (preg_match('/(\d+)\s+routes\s+\((\d+)\s+filtered\)/', $output, $matches)) { $counts['total'] = (int) $matches[1]; $counts['filtered'] = (int) $matches[2]; } return $counts; } /** * Parse BIRD status output * * @param string $output * @return array */ public static function parseStatus(string $output): array { $status = [ 'version' => '', 'router_id' => '', 'hostname' => '', 'server_time' => '', 'last_reboot' => '', 'last_reconfiguration' => '', ]; $lines = explode("\n", trim($output)); foreach ($lines as $line) { $line = trim($line); if (preg_match('/^BIRD\s+(.+)$/', $line, $matches)) { $status['version'] = trim($matches[1]); } elseif (preg_match('/^Router ID is\s+(.+)$/', $line, $matches)) { $status['router_id'] = trim($matches[1]); } elseif (preg_match('/^Hostname is\s+(.+)$/', $line, $matches)) { $status['hostname'] = trim($matches[1]); } elseif (preg_match('/^Current server time is\s+(.+)$/', $line, $matches)) { $status['server_time'] = trim($matches[1]); } elseif (preg_match('/^Last reboot on\s+(.+)$/', $line, $matches)) { $status['last_reboot'] = trim($matches[1]); } elseif (preg_match('/^Last reconfiguration on\s+(.+)$/', $line, $matches)) { $status['last_reconfiguration'] = trim($matches[1]); } } return $status; } /** * Parse detailed protocol info * * @param string $output * @return array */ public static function parseProtocolDetail(string $output): array { $info = [ 'name' => '', 'description' => '', 'state' => '', 'table' => '', 'neighbor_address' => '', 'neighbor_as' => '', 'local_as' => '', 'neighbor_id' => '', 'routes' => [ 'imported' => 0, 'exported' => 0, 'preferred' => 0, 'filtered' => 0, ], 'uptime' => '', 'hold_timer' => '', 'keepalive_timer' => '', 'last_error' => '', 'channel' => [], ]; $lines = explode("\n", trim($output)); $currentChannel = null; foreach ($lines as $line) { $line = trim($line); // Protocol name if (preg_match('/^(\S+)\s+BGP\s+/', $line, $matches)) { $info['name'] = $matches[1]; } // Description if (preg_match('/Description:\s+(.+)$/', $line, $matches)) { $info['description'] = trim($matches[1]); } // State if (preg_match('/BGP state:\s+(\S+)/', $line, $matches)) { $info['state'] = $matches[1]; } // Neighbor address and AS if (preg_match('/Neighbor address:\s+(\S+)/', $line, $matches)) { $info['neighbor_address'] = $matches[1]; } if (preg_match('/Neighbor AS:\s+(\d+)/', $line, $matches)) { $info['neighbor_as'] = (int) $matches[1]; } if (preg_match('/Local AS:\s+(\d+)/', $line, $matches)) { $info['local_as'] = (int) $matches[1]; } if (preg_match('/Neighbor ID:\s+(\S+)/', $line, $matches)) { $info['neighbor_id'] = $matches[1]; } // Routes if (preg_match('/Routes:\s+(\d+)\s+imported,\s+(\d+)\s+exported,\s+(\d+)\s+preferred/', $line, $matches)) { $info['routes']['imported'] = (int) $matches[1]; $info['routes']['exported'] = (int) $matches[2]; $info['routes']['preferred'] = (int) $matches[3]; } if (preg_match('/(\d+)\s+filtered/', $line, $matches)) { $info['routes']['filtered'] = (int) $matches[1]; } // Hold timer if (preg_match('/Hold timer:\s+(\S+)/', $line, $matches)) { $info['hold_timer'] = $matches[1]; } // Keepalive timer if (preg_match('/Keepalive timer:\s+(\S+)/', $line, $matches)) { $info['keepalive_timer'] = $matches[1]; } // Last error if (preg_match('/Last error:\s+(.+)$/', $line, $matches)) { $info['last_error'] = trim($matches[1]); } // Channel info if (preg_match('/Channel (ipv[46])/', $line, $matches)) { $currentChannel = $matches[1]; $info['channel'][$currentChannel] = []; } if ($currentChannel && preg_match('/State:\s+(\S+)/', $line, $matches)) { $info['channel'][$currentChannel]['state'] = $matches[1]; } if ($currentChannel && preg_match('/Table:\s+(\S+)/', $line, $matches)) { $info['channel'][$currentChannel]['table'] = $matches[1]; } } return $info; } /** * Get state color class for UI * * @param string $state * @return string */ public static function getStateColorClass(string $state): string { return match (strtolower($state)) { 'established' => 'color-fg-success', 'up' => 'color-fg-success', 'start', 'connect', 'active', 'opensent', 'openconfirm' => 'color-fg-attention', 'idle' => 'color-fg-muted', 'down' => 'color-fg-danger', default => 'color-fg-default', }; } /** * Get state label for UI * * @param string $state * @return string */ public static function getStateLabel(string $state): string { return match (strtolower($state)) { 'established' => 'Label--success', 'up' => 'Label--success', 'start', 'connect', 'active', 'opensent', 'openconfirm' => 'Label--attention', 'idle' => 'Label--secondary', 'down' => 'Label--danger', default => 'Label--primary', }; } /** * Generate basic BIRD 3 configuration * * @param array $config * @return string */ public static function generateBasicConfig(array $config): string { $bird = "# BIRD 3 Configuration\n"; $bird .= "# Generated by Pathvector Admin\n\n"; // Router ID if (!empty($config['router_id'])) { $bird .= "router id {$config['router_id']};\n\n"; } // Log $bird .= "log syslog all;\n\n"; // Debug $bird .= "debug protocols all;\n\n"; // Timeformat $bird .= "timeformat protocol iso long;\n\n"; // Protocol device $bird .= "protocol device {\n"; $bird .= " scan time 10;\n"; $bird .= "}\n\n"; // Protocol direct $bird .= "protocol direct {\n"; $bird .= " ipv4;\n"; $bird .= " ipv6;\n"; $bird .= "}\n\n"; // Protocol kernel $bird .= "protocol kernel {\n"; $bird .= " ipv4 {\n"; $bird .= " export all;\n"; $bird .= " import all;\n"; $bird .= " };\n"; $bird .= "}\n\n"; $bird .= "protocol kernel {\n"; $bird .= " ipv6 {\n"; $bird .= " export all;\n"; $bird .= " import all;\n"; $bird .= " };\n"; $bird .= "}\n\n"; return $bird; } /** * Validate BIRD config syntax (basic) * * @param string $config * @return array */ public static function validateSyntax(string $config): array { $errors = []; $warnings = []; // Check for common issues $lines = explode("\n", $config); $braceCount = 0; $inString = false; foreach ($lines as $lineNum => $line) { $lineNum++; // 1-indexed // Skip comments if (preg_match('/^\s*#/', $line)) { continue; } // Count braces for ($i = 0; $i < strlen($line); $i++) { $char = $line[$i]; if ($char === '"' && ($i === 0 || $line[$i - 1] !== '\\')) { $inString = !$inString; } if (!$inString) { if ($char === '{') { $braceCount++; } elseif ($char === '}') { $braceCount--; } } } // Check for missing semicolons (basic check) $trimmed = trim($line); if (!empty($trimmed) && !preg_match('/[{};#]$/', $trimmed) && !preg_match('/^\s*(protocol|filter|function|table|if|else|for|case|switch)/', $trimmed)) { $warnings[] = "Line $lineNum: Possible missing semicolon"; } } if ($braceCount !== 0) { $errors[] = "Unbalanced braces: " . ($braceCount > 0 ? "missing $braceCount closing braces" : "extra " . abs($braceCount) . " closing braces"); } return [ 'valid' => empty($errors), 'errors' => $errors, 'warnings' => $warnings, ]; } /** * Format BIRD configuration * * @param string $config * @return string */ public static function formatConfig(string $config): string { $lines = explode("\n", $config); $output = []; $indent = 0; foreach ($lines as $line) { $trimmed = trim($line); // Decrease indent for closing braces if (preg_match('/^}/', $trimmed)) { $indent = max(0, $indent - 1); } // Add line with proper indentation if (!empty($trimmed)) { $output[] = str_repeat("\t", $indent) . $trimmed; } else { $output[] = ''; } // Increase indent for opening braces if (preg_match('/{\s*$/', $trimmed)) { $indent++; } } return implode("\n", $output); } /** * Get list of BIRD 3 protocol types * * @return array */ public static function getProtocolTypes(): array { return [ 'bgp' => 'Border Gateway Protocol', 'ospf' => 'Open Shortest Path First', 'rip' => 'Routing Information Protocol', 'babel' => 'Babel Routing Protocol', 'static' => 'Static Routes', 'kernel' => 'Kernel Route Table', 'device' => 'Device Protocol', 'direct' => 'Direct Interfaces', 'pipe' => 'Table Pipe', 'bfd' => 'Bidirectional Forwarding Detection', 'rpki' => 'Resource Public Key Infrastructure', 'mrt' => 'MRT Table Dump', ]; } /** * Get BGP session states * * @return array */ public static function getBgpStates(): array { return [ 'Idle' => 'Not running', 'Connect' => 'Connecting to peer', 'Active' => 'Trying to connect', 'OpenSent' => 'Open message sent', 'OpenConfirm' => 'Waiting for keepalive', 'Established' => 'Session established', ]; } }