Files
Joseph.Rawlings d8b76233c0 Add nodes management and initial setup pages
- Implemented nodes management functionality in `nodes.php` including create, update, and delete actions.
- Added form validation and error handling for node operations.
- Created a new setup page in `setup.php` for initial administrator account creation.
- Included user feedback messages for successful operations and errors.
- Designed user interface for both nodes management and setup processes.
2025-12-14 01:33:12 -05:00

639 lines
18 KiB
PHP

<?php
/**
* Validator Class
*
* Validates Pathvector configurations and ensures correctness before generating BIRD 3 config
*/
class Validator {
private array $errors = [];
private array $warnings = [];
// Valid ASN ranges
private const ASN_MIN = 1;
private const ASN_MAX = 4294967295;
private const PRIVATE_ASN_START_16 = 64512;
private const PRIVATE_ASN_END_16 = 65534;
private const PRIVATE_ASN_START_32 = 4200000000;
private const PRIVATE_ASN_END_32 = 4294967294;
// Valid BGP roles (RFC 9234)
private const VALID_BGP_ROLES = ['provider', 'rs-server', 'rs-client', 'customer', 'peer'];
// Valid limit violation actions
private const VALID_LIMIT_ACTIONS = ['disable', 'restart', 'block', 'warn'];
// Transit-free ASNs (tier-1 networks)
private const TRANSIT_FREE_ASNS = [
174, 209, 286, 701, 1239, 1299, 2828, 2914, 3257, 3320, 3356, 3491,
5511, 6453, 6461, 6762, 6830, 7018, 12956
];
/**
* Get validation errors
*
* @return array
*/
public function getErrors(): array {
return $this->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();
}
}