File: //opt/cpguard/app/scripts/csf_migration.php
#!/opt/cpguard/cpg-php-fpm/bin/php
<?php
require dirname(__DIR__) . '/vendor/autoload.php';
use cPGuard\Core\Firewall\Firewall;
use Helpers\IP;
// ANSI color codes for better CLI display
const COLORS = [
'RED' => "\e[31m",
'GREEN' => "\e[32m",
'YELLOW' => "\e[33m",
'BLUE' => "\e[34m",
'MAGENTA' => "\e[35m",
'CYAN' => "\e[36m",
'WHITE' => "\e[37m",
'BOLD' => "\e[1m",
'RESET' => "\e[0m"
];
class CSFMigrator
{
private $errors = [];
private $warnings = [];
private $migrationData = [];
public function run()
{
// Simple one-line header
echo COLORS['CYAN'] . COLORS['BOLD'] . "CSF to cPGuard Migration Tool" . COLORS['RESET'] . "\n\n";
try {
// Quick pre-flight checks
if (!$this->performQuickChecks()) {
$this->displayErrors();
exit(1);
}
// Analysis phase with simple progress
echo COLORS['CYAN'] . "Analyzing CSF files and configuration..." . COLORS['RESET'] . "";
$this->prepareData();
if (!empty($this->errors)) {
echo COLORS['RED'] . "\nAnalysis failed:" . COLORS['RESET'] . "\n";
$this->displayErrors();
exit(1);
}
sleep(1);
// Display summary
echo "\r\033[K\033[36m" . "MIGRATION SUMMARY" . COLORS['RESET'] . "\033[0m\n\n";
$this->displaySummary();
// Confirm migration
if (!$this->confirmMigration()) {
echo COLORS['YELLOW'] . "Migration cancelled." . COLORS['RESET'] . "\n";
exit(0);
}
// Execute migration
echo "Executing migration..." . COLORS['RESET'] . "";
$this->executeMigration();
sleep(1);
// Complete
$this->displayCompletion();
} catch (Exception $e) {
echo COLORS['RED'] . "Fatal error: " . $e->getMessage() . COLORS['RESET'] . "\n";
exit(1);
}
}
private function performQuickChecks()
{
if (!file_exists('/etc/csf/csf.conf')) {
$this->errors[] = "CSF is not installed on this server";
return false;
}
if (!is_writable('/opt/cpguard')) {
$this->errors[] = "cPGuard directory is not writable";
return false;
}
if (!class_exists('Helpers\IP')) {
$this->errors[] = "Required IP helper class not found";
return false;
}
return true;
}
private function prepareData()
{
$existingWhitelistIPs = $this->getExistingIPs('/opt/cpguard/whitelistips.txt');
// Prepare csf.allow
$this->migrationData['csf_allow'] = $this->prepareIPsFromFile('/etc/csf/csf.allow', $existingWhitelistIPs, 'csf.allow');
// Prepare csf.ignore
$this->migrationData['csf_ignore'] = $this->prepareIPsFromFile('/etc/csf/csf.ignore', $existingWhitelistIPs, 'csf.ignore');
// Prepare blacklist IPs
$this->migrationData['blacklist'] = $this->prepareBlacklistIPs();
// Prepare configuration
$this->migrationData['config'] = $this->prepareConfigData();
// Check for country code conflicts
$this->checkCountryCodeConflicts();
// Combine whitelist data for migration
$this->migrationData['whitelist_combined'] = $this->combineWhitelistData();
$this->validatePreparedData();
}
private function checkCountryCodeConflicts()
{
$allowCountries = $this->migrationData['config']['CC_ALLOW']['valid'] ?? [];
$denyCountries = $this->migrationData['config']['CC_DENY']['valid'] ?? [];
if (empty($allowCountries) || empty($denyCountries)) {
return; // No conflict possible if one list is empty
}
$conflicts = array_intersect($allowCountries, $denyCountries);
if (!empty($conflicts)) {
$conflictList = implode(', ', $conflicts);
$this->errors[] = "Country code conflict detected: The following countries appear in both CC_ALLOW and CC_DENY: $conflictList";
$this->errors[] = "Please resolve this conflict in CSF configuration before migration";
}
}
private function prepareIPsFromFile($filename, $existingIPs, $sourceName)
{
if (!file_exists($filename)) {
return [
'source' => $sourceName,
'file_exists' => false,
'new_ips' => [],
'valid_lines' => [],
'invalid' => [],
'duplicates' => [],
'already_exists' => [],
'total_lines' => 0
];
}
$lines = file($filename, FILE_IGNORE_NEW_LINES);
$totalLines = count($lines);
$newIPs = [];
$validLines = [];
$invalidIPs = [];
$duplicatesInFile = [];
$alreadyExists = [];
$seenIPs = [];
$v6_count = 0;
$v4_count = 0;
foreach ($lines as $line) {
$originalLine = trim($line);
// Extract IP and comment
$parts = explode('#', $originalLine, 2);
$ipRange = trim($parts[0]);
$comment = isset($parts[1]) ? trim($parts[1]) : '';
if (empty($ipRange)) {
continue;
}
// Validate IP
if (!IP::validateRange($ipRange)) {
$invalidIPs[] = $ipRange;
continue;
}
// Check for duplicates within this file
if (in_array($ipRange, $seenIPs)) {
$duplicatesInFile[] = $ipRange;
continue;
}
$seenIPs[] = $ipRange;
// Check if IP already exists in cPGuard
if (in_array($ipRange, $existingIPs)) {
$alreadyExists[] = $ipRange;
continue;
}
// Create formatted line with comment handling
$formattedLine = $this->formatIPLine($ipRange, $comment, $sourceName);
$newIPs[] = $ipRange;
str_contains($ipRange, ':') ? $v6_count++ : $v4_count++;
$validLines[] = $formattedLine;
}
return [
'source' => $sourceName,
'file_exists' => true,
'new_ips' => $newIPs,
'valid_lines' => $validLines,
'invalid' => $invalidIPs,
'duplicates' => $duplicatesInFile,
'already_exists' => $alreadyExists,
'total_lines' => $totalLines,
'v4_count' => $v4_count,
'v6_count' => $v6_count
];
}
private function formatIPLine($ipRange, $comment, $sourceName)
{
if (!empty($comment)) {
// Keep original comment
return "$ipRange # $comment";
} else {
// Add source information as comment
return "$ipRange # from $sourceName";
}
}
private function prepareBlacklistIPs()
{
$denyFile = '/etc/csf/csf.deny';
$existingBlacklistIPs = $this->getExistingIPs('/opt/cpguard/blacklistips.txt');
return $this->prepareIPsFromFile($denyFile, $existingBlacklistIPs, 'csf.deny');
}
private function combineWhitelistData()
{
$combinedValidLines = array_merge(
$this->migrationData['csf_allow']['valid_lines'],
$this->migrationData['csf_ignore']['valid_lines']
);
$combinedNewIPs = array_merge(
$this->migrationData['csf_allow']['new_ips'],
$this->migrationData['csf_ignore']['new_ips']
);
return [
'valid_lines' => $combinedValidLines,
'new_ips' => $combinedNewIPs,
'total_new' => count($combinedNewIPs)
];
}
private function prepareConfigData()
{
$csfConfig = $this->fetchCSFConfig();
$configMapping = [
'TCP_IN' => 'fw_ports_tcp_in',
'TCP_OUT' => 'fw_ports_tcp_out',
'UDP_IN' => 'fw_ports_udp_in',
'UDP_OUT' => 'fw_ports_udp_out',
'CC_DENY' => 'fw_blacklist_country',
'CC_ALLOW' => 'fw_whitelist_country'
];
$preparedConfig = [];
foreach ($configMapping as $csfKey => $cpguardKey) {
$data = $csfConfig[$csfKey] ?? [];
$validData = [];
$invalidData = [];
if (in_array($csfKey, ['TCP_IN', 'TCP_OUT', 'UDP_IN', 'UDP_OUT'])) {
foreach ($data as $port) {
$port = trim($port);
if (empty($port))
continue;
$validatedPort = $this->validatePort($port);
if ($validatedPort !== null) {
$validData[] = $validatedPort;
} else {
$invalidData[] = $port;
}
}
if (!empty($invalidData)) {
$this->errors[] = "Invalid port detected: The following ports are invalid: " . implode(',', $invalidData);
$this->errors[] = "Please resolve this conflict in CSF configuration before migration";
return;
}
} else {
foreach ($data as $countryCode) {
$countryCode = trim($countryCode);
if (empty($countryCode))
continue;
if ($this->countryExists($countryCode)) {
$validData[] = strtoupper($countryCode);
} else {
$invalidData[] = $countryCode;
}
}
}
$preparedConfig[$csfKey] = [
'cpguard_key' => $cpguardKey,
'valid' => $validData,
'invalid' => $invalidData,
'original_values' => $data
];
}
return $preparedConfig;
}
private function validatePreparedData()
{
$hasData = false;
if (!empty($this->migrationData['whitelist_combined']['new_ips']))
$hasData = true;
if (!empty($this->migrationData['blacklist']['new_ips']))
$hasData = true;
if (!empty($this->migrationData['config'])) {
foreach ($this->migrationData['config'] as $config) {
if (!empty($config['valid']))
$hasData = true;
}
}
if (!$hasData) {
$this->warnings[] = "No new data found to migrate";
}
}
private function displaySummary()
{
// IP Files Summary
$this->displayIPFileSummary('Whitelist IPs (csf.allow)', $this->migrationData['csf_allow']);
$this->displayIPFileSummary('Whitelist IPs (csf.ignore)', $this->migrationData['csf_ignore']);
// Combined whitelist summary
$totalNew = $this->migrationData['whitelist_combined']['total_new'];
if ($totalNew > 0) {
echo COLORS['GREEN'] . COLORS['BOLD'] . "└─ Combined: $totalNew whitelist IPs will be imported" . COLORS['RESET'] . "\n\n";
} else {
echo COLORS['YELLOW'] . "└─ Combined: No new whitelist IPs to import" . COLORS['RESET'] . "\n\n";
}
$this->displayIPFileSummary('Blacklist IPs (csf.deny)', $this->migrationData['blacklist']);
// Configuration Summary
echo COLORS['BOLD'] . "CONFIGURATION SETTINGS" . COLORS['RESET'] . "\n\n";
foreach ($this->migrationData['config'] as $key => $config) {
$validCount = count($config['valid']);
$invalidCount = count($config['invalid']);
$originalCount = count($config['original_values']);
if ($originalCount == 0) {
echo COLORS['YELLOW'] . " $key: No configuration found" . COLORS['RESET'] . "\n";
continue;
}
echo COLORS['BOLD'] . " $key:" . COLORS['RESET'] . "\n";
if ($validCount > 0) {
echo " " . COLORS['GREEN'] . "✓ Valid ($validCount): " . COLORS['RESET'];
$this->displayWrappedValues($config['valid'], 4);
}
if ($invalidCount > 0) {
echo " " . COLORS['RED'] . "✗ Invalid ($invalidCount): " . COLORS['RESET'];
$this->displayWrappedValues($config['invalid'], 4);
}
echo "\n";
}
// Display warnings if any
if (!empty($this->warnings)) {
echo COLORS['YELLOW'] . COLORS['BOLD'] . "WARNINGS" . COLORS['RESET'] . "\n\n";
foreach ($this->warnings as $warning) {
echo " • " . $warning . "\n";
}
echo "\n";
}
}
private function displayIPFileSummary($title, $data)
{
echo COLORS['BOLD'] . $title . ":" . COLORS['RESET'] . "\n";
if (!$data['file_exists']) {
echo " " . COLORS['YELLOW'] . "File not found" . COLORS['RESET'] . "\n\n";
return;
}
$newCount = count($data['new_ips']);
$duplicateCount = count($data['duplicates']);
$invalidCount = count($data['invalid']);
$alreadyExistsCount = count($data['already_exists']);
printf(
" %-15s %s%-3d%s will be imported\n",
'New IPs:',
$newCount > 0 ? COLORS['GREEN'] : COLORS['YELLOW'],
$newCount,
COLORS['RESET']
);
printf(
" %-15s %s%-3d%s ips\n",
'IP v4:',
COLORS['RED'],
$data['v4_count'],
COLORS['RESET']
);
printf(
" %-15s %s%-3d%s ips\n",
'IP v6:',
COLORS['RED'],
$data['v6_count'],
COLORS['RESET']
);
printf(" %-15s %-3d already exist\n", 'Existing IPs:', $alreadyExistsCount);
if ($duplicateCount > 0) {
printf(
" %-15s %s%-3d%s duplicates skipped\n",
'Duplicates:',
COLORS['YELLOW'],
$duplicateCount,
COLORS['RESET']
);
}
if ($invalidCount > 0) {
printf(
" %-15s %s%-3d%s comment/unsupported entries\n",
'Skipped lines:',
COLORS['RED'],
$invalidCount,
COLORS['RESET']
);
}
echo "\n";
}
private function displayWrappedValues($values, $indent = 0)
{
$indentStr = str_repeat(' ', $indent);
$lineLength = 80 - $indent;
$currentLine = '';
foreach ($values as $i => $value) {
$separator = ($i < count($values) - 1) ? ', ' : '';
$addition = $value . $separator;
if (strlen($currentLine . $addition) > $lineLength && !empty($currentLine)) {
echo $currentLine . "\n" . $indentStr;
$currentLine = $addition;
} else {
$currentLine .= $addition;
}
}
if (!empty($currentLine)) {
echo $currentLine . "\n";
}
}
private function confirmMigration()
{
echo COLORS['YELLOW'] . COLORS['BOLD'] . "Proceed with migration? [y/N]: " . COLORS['RESET'];
$handle = fopen("php://stdin", "r");
$line = fgets($handle);
fclose($handle);
return trim(strtolower($line)) === 'y' || trim(strtolower($line)) === 'yes';
}
private function executeMigration()
{
$steps = [];
if (!empty($this->migrationData['whitelist_combined']['valid_lines'])) {
$count = $this->migrationData['whitelist_combined']['total_new'];
$steps[] = ['type' => 'whitelist', 'count' => $count, 'desc' => "whitelist IPs"];
}
if (!empty($this->migrationData['blacklist']['valid_lines'])) {
$count = count($this->migrationData['blacklist']['new_ips']);
$steps[] = ['type' => 'blacklist', 'count' => $count, 'desc' => "blacklist IPs"];
}
foreach ($this->migrationData['config'] as $key => $config) {
if (!empty($config['valid'])) {
$count = count($config['valid']);
$steps[] = ['type' => 'config', 'key' => $key, 'config' => $config, 'count' => $count, 'desc' => $key];
}
}
foreach ($steps as $i => $step) {
switch ($step['type']) {
case 'whitelist':
$this->migrateIPs($this->migrationData['whitelist_combined']['valid_lines'], '/opt/cpguard/whitelistips.txt');
break;
case 'blacklist':
$this->migrateIPs($this->migrationData['blacklist']['valid_lines'], '/opt/cpguard/blacklistips.txt');
break;
case 'config':
$this->updateConfig($step['config']['cpguard_key'], $step['config']['valid']);
break;
}
}
}
private function displayCompletion()
{
echo "\r\033[K\033\n[32m✓ Migration Completed Successfully!\033[0m\n\n";
// Migration results
$whitelistTotal = $this->migrationData['whitelist_combined']['total_new'];
$blacklistTotal = count($this->migrationData['blacklist']['new_ips']);
if ($whitelistTotal > 0 || $blacklistTotal > 0) {
echo COLORS['BOLD'] . "Migration Results:" . COLORS['RESET'] . "\n";
if ($whitelistTotal > 0) {
echo " • $whitelistTotal whitelist IPs imported\n";
}
if ($blacklistTotal > 0) {
echo " • $blacklistTotal blacklist IPs imported\n";
}
foreach ($this->migrationData['config'] as $key => $config) {
$count = count($config['valid']);
if ($count > 0) {
echo " • $key: $count items imported\n";
}
}
echo "\n";
}
$this->fw_restart();
}
private function displayErrors()
{
if (!empty($this->errors)) {
foreach ($this->errors as $error) {
echo " " . COLORS['RED'] . "✗ " . $error . COLORS['RESET'] . "\n";
}
}
}
// Helper methods
private function getExistingIPs($file)
{
if (!file_exists($file)) {
return [];
}
$ips = [];
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (preg_match('/\s*([0-9a-fA-F:.\/\-]+)[\s#]*(.*)$/m', $line, $matches)) {
$ips[] = trim($matches[1]);
}
}
return $ips;
}
private function fetchCSFConfig()
{
$data = [];
$fileCsfConf = '/etc/csf/csf.conf';
if (!file_exists($fileCsfConf)) {
return $data;
}
$csfConf = file_get_contents($fileCsfConf);
$patterns = [
'TCP_IN' => '/^\s*TCP_IN\s*\=\s*\"([^\"]*)\"/m',
'UDP_IN' => '/^\s*UDP_IN\s*\=\s*\"([^\"]*)\"/m',
'TCP_OUT' => '/^\s*TCP_OUT\s*\=\s*\"([^\"]*)\"/m',
'UDP_OUT' => '/^\s*UDP_OUT\s*\=\s*\"([^\"]*)\"/m',
'CC_DENY' => '/^\s*CC_DENY\s*\=\s*\"([^\"]*)\"/m',
'CC_ALLOW' => '/^\s*CC_ALLOW\s*\=\s*\"([^\"]*)\"/m'
];
foreach ($patterns as $key => $pattern) {
if (preg_match($pattern, $csfConf, $matches)) {
$data[$key] = array_filter(array_map('trim', explode(',', $matches[1])));
}
}
return $data;
}
private function validatePort($port)
{
if (empty($port)) {
return null;
}
if (strpos($port, ':') !== false) {
$port = str_replace(':', '-', $port);
}
if (strpos($port, '-') !== false) {
$range = explode('-', $port);
if (count($range) !== 2) {
return null;
}
$start = (int) trim($range[0]);
$end = (int) trim($range[1]);
if ($start < 1 || $start > 65535 || $end < 1 || $end > 65535 || $start > $end) {
return null;
}
return "$start-$end";
} else {
if (!is_numeric($port) || $port < 1 || $port > 65535) {
return null;
}
return (string) (int) $port;
}
}
private function countryExists($countryCode)
{
$geoDir = ROOT . '/resources/geoip/';
return file_exists($geoDir . strtolower($countryCode) . '.zone');
}
private function migrateIPs($lines, $file)
{
if (!empty($lines)) {
file_put_contents($file, implode(PHP_EOL, $lines) . PHP_EOL, FILE_APPEND | LOCK_EX);
}
}
private function updateConfig($key, $values)
{
config($key, $values);
}
private function fw_restart()
{
$fw = new Firewall();
$fw->disable();
if (!config('fw_switch')) {
echo "Firewall is disabled. Changes not applied\n";
return false;
}
echo "Restarting firewall... This may take a few seconds...";
if ($fw->enable()) {
echo "\r\e[KFirewall \e[32mrestarted\e[0m" . PHP_EOL;
} else {
echo "\r\e[K\e[31mFirewall restart failed\e[0m" . PHP_EOL;
}
}
}
// Main execution
if (!file_exists('/etc/csf/csf.conf')) {
echo COLORS['RED'] . "CSF is not installed on this server" . COLORS['RESET'] . "\n";
exit(1);
}
$migrator = new CSFMigrator();
$migrator->run();