errors; } /** * Get validation warnings * * @return array */ public function getWarnings(): array { return $this->warnings; } /** * Check if validation passed * * @return bool */ public function isValid(): bool { return empty($this->errors); } /** * Clear errors and warnings * * @return void */ public function clear(): void { $this->errors = []; $this->warnings = []; } /** * Add error * * @param string $field * @param string $message * @return void */ private function addError(string $field, string $message): void { $this->errors[$field] = $message; } /** * Add warning * * @param string $field * @param string $message * @return void */ private function addWarning(string $field, string $message): void { $this->warnings[$field] = $message; } /** * Validate ASN * * @param mixed $asn * @param string $field * @return bool */ public function validateAsn($asn, string $field = 'asn'): bool { if (!is_numeric($asn)) { $this->addError($field, 'ASN must be a number'); return false; } $asn = (int) $asn; if ($asn < self::ASN_MIN || $asn > self::ASN_MAX) { $this->addError($field, "ASN must be between " . self::ASN_MIN . " and " . self::ASN_MAX); return false; } // Warn about reserved/private ASNs if ($asn === 0) { $this->addError($field, 'ASN 0 is reserved'); return false; } if ($asn === 23456) { $this->addError($field, 'ASN 23456 is reserved for AS_TRANS (RFC 6793)'); return false; } if (($asn >= self::PRIVATE_ASN_START_16 && $asn <= self::PRIVATE_ASN_END_16) || ($asn >= self::PRIVATE_ASN_START_32 && $asn <= self::PRIVATE_ASN_END_32)) { $this->addWarning($field, 'This is a private ASN'); } return true; } /** * Validate IPv4 address * * @param string $ip * @param string $field * @return bool */ public function validateIPv4(string $ip, string $field = 'ip'): bool { if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $this->addError($field, 'Invalid IPv4 address'); return false; } return true; } /** * Validate IPv6 address * * @param string $ip * @param string $field * @return bool */ public function validateIPv6(string $ip, string $field = 'ip'): bool { if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->addError($field, 'Invalid IPv6 address'); return false; } return true; } /** * Validate IP address (IPv4 or IPv6) * * @param string $ip * @param string $field * @return bool */ public function validateIP(string $ip, string $field = 'ip'): bool { if (!filter_var($ip, FILTER_VALIDATE_IP)) { $this->addError($field, 'Invalid IP address'); return false; } return true; } /** * Validate IPv4 prefix * * @param string $prefix * @param string $field * @return bool */ public function validateIPv4Prefix(string $prefix, string $field = 'prefix'): bool { $parts = explode('/', $prefix); if (count($parts) !== 2) { $this->addError($field, 'Invalid prefix format (expected x.x.x.x/xx)'); return false; } [$ip, $length] = $parts; if (!$this->validateIPv4($ip, $field)) { return false; } $length = (int) $length; if ($length < 0 || $length > 32) { $this->addError($field, 'Prefix length must be between 0 and 32'); return false; } return true; } /** * Validate IPv6 prefix * * @param string $prefix * @param string $field * @return bool */ public function validateIPv6Prefix(string $prefix, string $field = 'prefix'): bool { $parts = explode('/', $prefix); if (count($parts) !== 2) { $this->addError($field, 'Invalid prefix format (expected xxxx::/xx)'); return false; } [$ip, $length] = $parts; if (!$this->validateIPv6($ip, $field)) { return false; } $length = (int) $length; if ($length < 0 || $length > 128) { $this->addError($field, 'Prefix length must be between 0 and 128'); return false; } return true; } /** * Validate prefix (IPv4 or IPv6) * * @param string $prefix * @param string $field * @return bool */ public function validatePrefix(string $prefix, string $field = 'prefix'): bool { if (strpos($prefix, ':') !== false) { return $this->validateIPv6Prefix($prefix, $field); } return $this->validateIPv4Prefix($prefix, $field); } /** * Validate router ID * * @param string $routerId * @param string $field * @return bool */ public function validateRouterId(string $routerId, string $field = 'router_id'): bool { return $this->validateIPv4($routerId, $field); } /** * Validate BGP community (standard) * * @param string $community * @param string $field * @return bool */ public function validateStandardCommunity(string $community, string $field = 'community'): bool { // Format: ASN,value or ASN:value $pattern = '/^(\d+)[,:](\d+)$/'; if (!preg_match($pattern, $community, $matches)) { $this->addError($field, 'Invalid standard community format (expected ASN,value or ASN:value)'); return false; } $asn = (int) $matches[1]; $value = (int) $matches[2]; if ($asn > 65535 || $value > 65535) { $this->addError($field, 'Standard community values must be <= 65535'); return false; } return true; } /** * Validate BGP large community * * @param string $community * @param string $field * @return bool */ public function validateLargeCommunity(string $community, string $field = 'community'): bool { // Format: ASN:value1:value2 $pattern = '/^(\d+):(\d+):(\d+)$/'; if (!preg_match($pattern, $community, $matches)) { $this->addError($field, 'Invalid large community format (expected ASN:value1:value2)'); return false; } $asn = (int) $matches[1]; $val1 = (int) $matches[2]; $val2 = (int) $matches[3]; if ($asn > self::ASN_MAX || $val1 > self::ASN_MAX || $val2 > self::ASN_MAX) { $this->addError($field, 'Large community values must be <= 4294967295'); return false; } return true; } /** * Validate community (standard or large) * * @param string $community * @param string $field * @return bool */ public function validateCommunity(string $community, string $field = 'community'): bool { // Try large community first (has 3 parts) if (preg_match('/^\d+:\d+:\d+$/', $community)) { return $this->validateLargeCommunity($community, $field); } return $this->validateStandardCommunity($community, $field); } /** * Validate BGP role (RFC 9234) * * @param string $role * @param string $field * @return bool */ public function validateBgpRole(string $role, string $field = 'role'): bool { $role = strtolower(str_replace('_', '-', $role)); if (!in_array($role, self::VALID_BGP_ROLES)) { $this->addError($field, 'Invalid BGP role. Must be one of: ' . implode(', ', self::VALID_BGP_ROLES)); return false; } return true; } /** * Validate TCP port * * @param mixed $port * @param string $field * @return bool */ public function validatePort(mixed $port, string $field = 'port'): bool { if (!is_numeric($port)) { $this->addError($field, 'Port must be a number'); return false; } $port = (int) $port; if ($port < 1 || $port > 65535) { $this->addError($field, 'Port must be between 1 and 65535'); return false; } return true; } /** * Validate limit violation action * * @param string $action * @param string $field * @return bool */ public function validateLimitAction(string $action, string $field = 'action'): bool { if (!in_array($action, self::VALID_LIMIT_ACTIONS)) { $this->addError($field, 'Invalid limit action. Must be one of: ' . implode(', ', self::VALID_LIMIT_ACTIONS)); return false; } return true; } /** * Validate hostname * * @param string $hostname * @param string $field * @return bool */ public function validateHostname(string $hostname, string $field = 'hostname'): bool { $pattern = '/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/'; if (!preg_match($pattern, $hostname)) { $this->addError($field, 'Invalid hostname format'); return false; } if (strlen($hostname) > 253) { $this->addError($field, 'Hostname must not exceed 253 characters'); return false; } return true; } /** * Validate AS-SET name * * @param string $asSet * @param string $field * @return bool */ public function validateAsSet(string $asSet, string $field = 'as_set'): bool { // AS-SET names typically start with AS and can contain alphanumeric chars, hyphens, and colons $pattern = '/^(AS-?)?[A-Z0-9][A-Z0-9-:]*$/i'; if (!preg_match($pattern, $asSet)) { $this->addError($field, 'Invalid AS-SET name format'); return false; } return true; } /** * Validate peer configuration * * @param array $peer * @return bool */ public function validatePeerConfig(array $peer): bool { $this->clear(); // Required fields if (empty($peer['asn'])) { $this->addError('asn', 'ASN is required'); } else { $this->validateAsn($peer['asn'], 'asn'); } if (empty($peer['neighbors']) || !is_array($peer['neighbors']) || count($peer['neighbors']) === 0) { $this->addError('neighbors', 'At least one neighbor IP is required'); } else { foreach ($peer['neighbors'] as $i => $neighbor) { $this->validateIP($neighbor, "neighbors[$i]"); } } // Optional fields validation if (!empty($peer['remote_asn'])) { $this->validateAsn($peer['remote_asn'], 'remote_asn'); } if (!empty($peer['local_asn'])) { $this->validateAsn($peer['local_asn'], 'local_asn'); } if (!empty($peer['local_port'])) { $this->validatePort($peer['local_port'], 'local_port'); } if (!empty($peer['neighbor_port'])) { $this->validatePort($peer['neighbor_port'], 'neighbor_port'); } if (!empty($peer['role'])) { $this->validateBgpRole($peer['role'], 'role'); } if (!empty($peer['router_id'])) { $this->validateRouterId($peer['router_id'], 'router_id'); } if (!empty($peer['receive_limit_violation'])) { $this->validateLimitAction($peer['receive_limit_violation'], 'receive_limit_violation'); } if (!empty($peer['export_limit_violation'])) { $this->validateLimitAction($peer['export_limit_violation'], 'export_limit_violation'); } // Validate communities if (!empty($peer['add_on_import']) && is_array($peer['add_on_import'])) { foreach ($peer['add_on_import'] as $i => $community) { $this->validateCommunity($community, "add_on_import[$i]"); } } if (!empty($peer['add_on_export']) && is_array($peer['add_on_export'])) { foreach ($peer['add_on_export'] as $i => $community) { $this->validateCommunity($community, "add_on_export[$i]"); } } if (!empty($peer['announce']) && is_array($peer['announce'])) { foreach ($peer['announce'] as $i => $community) { $this->validateCommunity($community, "announce[$i]"); } } // Validate prefixes if (!empty($peer['prefixes']) && is_array($peer['prefixes'])) { foreach ($peer['prefixes'] as $i => $prefix) { $this->validatePrefix($prefix, "prefixes[$i]"); } } // Validate as-set if (!empty($peer['as_set'])) { $this->validateAsSet($peer['as_set'], 'as_set'); } // Logical validation if (!empty($peer['only_announce']) && !empty($peer['announce_all']) && $peer['announce_all']) { $this->addError('announce_all', 'only-announce and announce-all cannot both be true'); } if (!empty($peer['default_local_pref']) && !empty($peer['optimize_inbound']) && $peer['optimize_inbound']) { $this->addError('optimize_inbound', 'default-local-pref and optimize-inbound cannot both be set'); } return $this->isValid(); } /** * Validate global config * * @param array $config * @return bool */ public function validateGlobalConfig(array $config): bool { $this->clear(); // Required fields if (empty($config['asn'])) { $this->addError('asn', 'ASN is required'); } else { $this->validateAsn($config['asn'], 'asn'); } if (empty($config['router_id'])) { $this->addError('router_id', 'Router ID is required'); } else { $this->validateRouterId($config['router_id'], 'router_id'); } // Optional fields if (!empty($config['source4'])) { $this->validateIPv4($config['source4'], 'source4'); } if (!empty($config['source6'])) { $this->validateIPv6($config['source6'], 'source6'); } // Validate prefixes if (!empty($config['prefixes']) && is_array($config['prefixes'])) { foreach ($config['prefixes'] as $i => $prefix) { $this->validatePrefix($prefix, "prefixes[$i]"); } } return $this->isValid(); } /** * Validate node configuration * * @param array $node * @return bool */ public function validateNodeConfig(array $node): bool { $this->clear(); if (empty($node['asn'])) { $this->addError('asn', 'ASN is required'); } else { $this->validateAsn($node['asn'], 'asn'); } if (!empty($node['router_id'])) { $this->validateRouterId($node['router_id'], 'router_id'); } if (!empty($node['hostname'])) { $this->validateHostname($node['hostname'], 'hostname'); } return $this->isValid(); } /** * Check if ASN is a transit-free network * * @param int $asn * @return bool */ public function isTransitFreeAsn(int $asn): bool { return in_array($asn, self::TRANSIT_FREE_ASNS); } /** * Check if ASN is private * * @param int $asn * @return bool */ public function isPrivateAsn(int $asn): bool { return ($asn >= self::PRIVATE_ASN_START_16 && $asn <= self::PRIVATE_ASN_END_16) || ($asn >= self::PRIVATE_ASN_START_32 && $asn <= self::PRIVATE_ASN_END_32); } /** * Validate required fields * * @param array $data * @param array $requiredFields * @return bool */ public function validateRequired(array $data, array $requiredFields): bool { foreach ($requiredFields as $field) { if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) { $this->addError($field, ucfirst(str_replace('_', ' ', $field)) . ' is required'); } } return $this->isValid(); } }