-
Notifications
You must be signed in to change notification settings - Fork 35
Add MikroTik NAT Sync to README #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
328e1b4
c3eaad7
8c1f235
a06ba19
19ff9c8
7d3c892
e11e9db
308d646
c122150
c4850d8
3877abe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
|
|
||
| # 🌐 MikroTik NAT Sync for Pelican Panel | ||
|
|
||
|  | ||
|  | ||
|  | ||
|
|
||
| **MikroTik NAT Sync** | ||
|
|
||
| --- | ||
|
|
||
| ## 🇺🇸 English | ||
|
|
||
| ### 🚀 Features | ||
| * **Full Automation**: Automatically creates/removes DST-NAT rules based on Pelican allocations. | ||
| * **Security First**: Define a "Forbidden Ports" list to protect sensitive services (SSH, SFTP, etc.). | ||
| * **Smart Tags**: Manages only its own rules using the `Pelican:` comment tag. | ||
| * **Easy Setup**: Configure everything (IP, credentials, intervals) directly in the Admin UI. | ||
|
|
||
| ### 🛠 MikroTik Configuration | ||
| Enable the REST API on your router to allow communication: | ||
| ```Bash | ||
| /ip service set www-ssl disabled=no port=9443 | ||
| ``` | ||
| Note: We recommend creating a dedicated user with specific firewall permissions. | ||
|
|
||
| ### 📦 Installation | ||
|
|
||
| **Method 1: Via Web Interface (Easiest)** | ||
| 1. In your Pelican Admin Panel, go to **Plugins** -> **Import**. | ||
| 2. Paste the URL or upload the downloaded ZIP file. | ||
| 3. Click **Install** and configure via the Gear icon. | ||
|
|
||
| Developed with AI Assistance (Gemini) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| <?php | ||
|
|
||
| return [ | ||
| // Config values for MikroTik NAT Sync | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "id": "mikrotik-nat-sync", | ||
| "name": "MikroTik NAT Sync", | ||
| "author": "avalon", | ||
| "version": "1.0.0", | ||
| "description": null, | ||
| "category": "plugin", | ||
| "url": null, | ||
| "update_url": null, | ||
| "namespace": "Avalon\\MikroTikNATSync", | ||
| "class": "MikroTikNATSyncPlugin", | ||
| "panels": null, | ||
| "panel_version": "1.0.0-beta32", | ||
| "composer_packages": null | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| <?php | ||
|
|
||
| namespace Avalon\MikroTikNATSync\Console\Commands; | ||
|
|
||
| use Illuminate\Console\Command; | ||
| use Illuminate\Support\Facades\DB; | ||
| use Illuminate\Support\Facades\Http; | ||
|
|
||
| class SyncMikrotikCommand extends Command | ||
| { | ||
| protected $signature = 'mikrotik:sync'; | ||
| protected $description = 'Synchronize NAT rules with MikroTik checking for forbidden ports'; | ||
|
|
||
| public function handle() | ||
| { | ||
| $this->info('Starting MikroTik Sync...'); | ||
|
|
||
| $mk_ip = str_replace(['http://', 'https://'], '', env('MIKROTIK_NAT_SYNC_IP')); | ||
| $mk_port = env('MIKROTIK_NAT_SYNC_PORT', '9080'); | ||
| $mk_user = env('MIKROTIK_NAT_SYNC_USER'); | ||
| $mk_pass = env('MIKROTIK_NAT_SYNC_PASSWORD'); | ||
| $mk_interface = env('MIKROTIK_NAT_SYNC_INTERFACE', 'ether1'); | ||
|
|
||
| // Get forbidden ports list and convert to array | ||
| $forbidden_string = env('MIKROTIK_NAT_SYNC_FORBIDDEN_PORTS', ''); | ||
| $forbidden_ports = array_map('trim', explode(',', $forbidden_string)); | ||
|
|
||
| if (!$mk_ip || !$mk_user || !$mk_pass) { | ||
| $this->error('MikroTik settings are not configured!'); | ||
| return; | ||
| } | ||
|
|
||
| if (str_contains($mk_ip, ':')) { | ||
| $url = "http://" . $mk_ip . "/rest/ip/firewall/nat"; | ||
| } else { | ||
| $url = "http://" . $mk_ip . ":" . $mk_port . "/rest/ip/firewall/nat"; | ||
| } | ||
|
|
||
| $active_servers = DB::table('servers') | ||
| ->join('allocations', 'allocations.server_id', '=', 'servers.id') | ||
| ->select('servers.uuid', 'servers.name', 'allocations.ip', 'allocations.port') | ||
| ->get(); | ||
|
Comment on lines
+39
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, find migration files related to servers table
find . -type f -name "*migration*" -o -name "*migrations*" | head -20Repository: pelican-dev/plugins Length of output: 318 🏁 Script executed: #!/bin/bash
# Search for server table migrations
fd -t f "migration" | grep -i server | head -10Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Look for the file referenced in the review
find . -path "*Console/Commands/SyncMikrotikCommand.php"Repository: pelican-dev/plugins Length of output: 127 🏁 Script executed: #!/bin/bash
# Search for create_servers migration or similar
rg -l "create.*table.*servers" --type php | head -10Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Check for schema definition in the repository
rg -n "Schema::create\('servers'" --type php -A 30 | head -80Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Read the actual file to see the code context
cat -n ./mikrotik-nat-sync/src/Console/Commands/SyncMikrotikCommand.php | head -100Repository: pelican-dev/plugins Length of output: 4787 🏁 Script executed: #!/bin/bash
# Explore the mikrotik-nat-sync directory structure
find ./mikrotik-nat-sync -type f -name "*.php" | head -20Repository: pelican-dev/plugins Length of output: 532 🏁 Script executed: #!/bin/bash
# Look for database configuration or models in the plugin
find ./mikrotik-nat-sync -type f \( -name "*migration*" -o -name "*model*" -o -name "*.sql" \) 2>/dev/nullRepository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Search for any Server model or schema definition across the entire repo
rg -n "class Server" --type php | head -10Repository: pelican-dev/plugins Length of output: 307 🏁 Script executed: #!/bin/bash
# Search for servers table references
rg -n "table.*servers\|servers.*table" --type php | head -20Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Search across entire repo for Server model definition
rg -n "^class Server" --type php | head -20Repository: pelican-dev/plugins Length of output: 307 🏁 Script executed: #!/bin/bash
# Look for any table definition or migration related to servers in the plugin structure
find . -type d -name "database" | head -10Repository: pelican-dev/plugins Length of output: 241 🏁 Script executed: #!/bin/bash
# Check if there's a parent application directory or if schemas are defined elsewhere
ls -la | head -20Repository: pelican-dev/plugins Length of output: 1302 🏁 Script executed: #!/bin/bash
# Search for any references to 'deleted_at' or 'installed_at' in the codebase
rg -n "deleted_at\|installed_at" --type php | head -30Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Check the plugin provider to understand the application context
cat ./mikrotik-nat-sync/src/Providers/MikroTikNATSyncPluginProvider.phpRepository: pelican-dev/plugins Length of output: 336 🏁 Script executed: #!/bin/bash
# Look for other references to servers table filtering in the codebase
rg -n "servers.*where\|where.*servers" --type php -B 2 -A 2 | head -50Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Check if any migrations in other plugins reference the servers table
rg -n "table.*servers" --type php -B 5 -A 10 | head -80Repository: pelican-dev/plugins Length of output: 8967 🏁 Script executed: #!/bin/bash
# Look for Server model usage across plugins
rg -n "\->servers\|DB::table\('servers'\)" --type php | head -30Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Check if there's a README or docs that explain the architecture
find . -name "README*" -o -name "*.md" | xargs grep -l "server\|database" | head -10Repository: pelican-dev/plugins Length of output: 321 🏁 Script executed: #!/bin/bash
# Search for any soft delete references or timestamps
rg -n "SoftDeletes\|timestamps\|deleted_at\|installed_at" --type php | head -40Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Look for the main application that these plugins extend
rg -n "namespace|use.*Server" ./mikrotik-nat-sync --type php | head -20Repository: pelican-dev/plugins Length of output: 703 🏁 Script executed: #!/bin/bash
# Check if there's a parent pelican application directory referenced
rg -n "pelican\|Pelican" --type php ./mikrotik-nat-sync | head -20Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Look for any status column references in the codebase
rg -n "'status'|\"status\"" --type php | grep -i server | head -20Repository: pelican-dev/plugins Length of output: 1816 🏁 Script executed: #!/bin/bash
# Check git history or comments for context about server filtering
rg -n "suspended\|uninstalled\|inactive\|deleted" --type php | head -30Repository: pelican-dev/plugins Length of output: 45 🏁 Script executed: #!/bin/bash
# Search for any Server model that might be imported or used
rg -n "Server::|->where|->get\(\)" ./mikrotik-nat-sync --type php -B 3 -A 3Repository: pelican-dev/plugins Length of output: 1645 🏁 Script executed: #!/bin/bash
# Look at the plugin structure to understand dependencies
cat ./mikrotik-nat-sync/composer.json 2>/dev/null || echo "No composer.json found"Repository: pelican-dev/plugins Length of output: 85 🏁 Script executed: #!/bin/bash
# Check if there's any documentation or comments about server states
find ./mikrotik-nat-sync -type f \( -name "*.md" -o -name "*.txt" \) -exec cat {} \;Repository: pelican-dev/plugins Length of output: 3066 🏁 Script executed: #!/bin/bash
# Search for Pelican application references to understand the context
rg -n "pelican\|Pelican" . --type php --type json | grep -i "server\|require\|depend" | head -20Repository: pelican-dev/plugins Length of output: 45 🌐 Web query:
💡 Result: In Pelican Panel, the Source: Pelican Panel “Server Model” (DeepWiki) [1] Query fetches all servers regardless of installation or suspension status. The DB query joins Suggested fix $active_servers = DB::table('servers')
->join('allocations', 'allocations.server_id', '=', 'servers.id')
+ ->where('servers.status', 'installed')
+ ->whereNotNull('servers.installed_at')
->select('servers.uuid', 'servers.name', 'allocations.ip', 'allocations.port')
->get();🤖 Prompt for AI Agents |
||
|
|
||
| $whitelist = []; | ||
| foreach ($active_servers as $srv) { | ||
| // CHECK: if port is in forbidden list | ||
| if (in_array((string)$srv->port, $forbidden_ports)) { | ||
| $this->warn("Port {$srv->port} for server {$srv->name} is FORBIDDEN. Skipping."); | ||
| continue; | ||
| } | ||
|
|
||
| $target_ip = ($srv->ip == '0.0.0.0') ? '192.168.70.231' : $srv->ip; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded private IP When a server allocation has IP Proposed fix- $target_ip = ($srv->ip == '0.0.0.0') ? '192.168.70.231' : $srv->ip;
+ $defaultIp = env('MIKROTIK_DEFAULT_TARGET_IP');
+ if ($srv->ip === '0.0.0.0') {
+ if (!$defaultIp) {
+ $this->warn("Server {$srv->name} has 0.0.0.0 allocation and no MIKROTIK_DEFAULT_TARGET_IP set. Skipping.");
+ continue;
+ }
+ $target_ip = $defaultIp;
+ } else {
+ $target_ip = $srv->ip;
+ }🤖 Prompt for AI Agents |
||
| $whitelist[$srv->port . '-tcp'] = ['ip' => $target_ip, 'name' => $srv->name, 'uuid' => $srv->uuid]; | ||
| $whitelist[$srv->port . '-udp'] = ['ip' => $target_ip, 'name' => $srv->name, 'uuid' => $srv->uuid]; | ||
| } | ||
|
|
||
| try { | ||
| $response = Http::withBasicAuth($mk_user, $mk_pass)->timeout(10)->get($url); | ||
| if (!$response->successful()) { | ||
| $this->error('API Error: ' . $response->body()); | ||
| return; | ||
| } | ||
|
|
||
| $existing_rules = []; | ||
| $rules_data = $response->json(); | ||
|
|
||
| // Safety check if response is array | ||
| if (!is_array($rules_data)) { | ||
| $this->error('Invalid response format from MikroTik.'); | ||
| return; | ||
| } | ||
|
|
||
| foreach ($rules_data as $rule) { | ||
| if (isset($rule['comment']) && str_contains($rule['comment'], 'Pelican:')) { | ||
| $dst_port = $rule['dst-port'] ?? ''; | ||
| $protocol = $rule['protocol'] ?? 'tcp'; | ||
| $key = $dst_port . '-' . $protocol; | ||
|
|
||
| // We need rule ID to delete it later if needed | ||
| if (isset($rule['.id'])) { | ||
| $existing_rules[$key] = $rule['.id']; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Remove old rules that are not in whitelist | ||
| foreach ($existing_rules as $key => $id) { | ||
| if (!isset($whitelist[$key])) { | ||
| $this->warn("Deleting rule: $key"); | ||
| Http::withBasicAuth($mk_user, $mk_pass)->delete("$url/$id"); | ||
| } | ||
| } | ||
|
|
||
| // Add new rules | ||
| foreach ($whitelist as $key => $info) { | ||
| if (!isset($existing_rules[$key])) { | ||
| [$port, $proto] = explode('-', $key); | ||
| $this->info("Adding rule: $key for {$info['name']}"); | ||
|
|
||
| $payload = [ | ||
| 'chain' => 'dstnat', | ||
| 'action' => 'dst-nat', | ||
| 'to-addresses' => $info['ip'], | ||
| 'to-ports' => (string)$port, | ||
| 'protocol' => $proto, | ||
| 'dst-port' => (string)$port, | ||
| 'in-interface' => $mk_interface, | ||
| 'comment' => "Pelican: {$info['name']} ({$info['uuid']})" | ||
| ]; | ||
|
|
||
| Http::withBasicAuth($mk_user, $mk_pass)->put($url, $payload); | ||
| } | ||
| } | ||
| $this->info('Sync Complete.'); | ||
| } catch (\Exception $e) { | ||
| $this->error('Error: ' . $e->getMessage()); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| <?php | ||
|
|
||
| namespace Avalon\MikroTikNATSync; | ||
|
|
||
| use Filament\Contracts\Plugin as FilamentPlugin; | ||
| use Filament\Panel; | ||
| use Filament\Forms\Components\TextInput; | ||
| use Filament\Forms\Components\Select; | ||
| use Filament\Notifications\Notification; | ||
| use App\Contracts\Plugins\HasPluginSettings; | ||
| use App\Traits\EnvironmentWriterTrait; | ||
| use Illuminate\Console\Scheduling\Schedule; | ||
|
|
||
| class MikroTikNATSyncPlugin implements FilamentPlugin, HasPluginSettings | ||
| { | ||
| use EnvironmentWriterTrait; | ||
|
|
||
| public function getId(): string | ||
| { | ||
| return 'mikrotik-nat-sync'; | ||
| } | ||
|
|
||
| public function register(Panel $panel): void | ||
| { | ||
| // | ||
| } | ||
|
|
||
| public function boot(Panel $panel): void | ||
| { | ||
| if (app()->runningInConsole()) { | ||
| $this->commands([ | ||
| \Avalon\MikroTikNATSync\Console\Commands\SyncMikrotikCommand::class, | ||
| ]); | ||
| } | ||
|
|
||
| app()->booted(function () { | ||
| $schedule = app(Schedule::class); | ||
| $interval = env('MIKROTIK_NAT_SYNC_INTERVAL', 'everyFiveMinutes'); | ||
|
|
||
| $schedule->command('mikrotik:sync') | ||
| ->{$interval}() | ||
| ->withoutOverlapping(); | ||
| }); | ||
| } | ||
|
|
||
| public function getSettingsForm(): array | ||
| { | ||
| return [ | ||
| TextInput::make('mk_ip') | ||
| ->label('MikroTik IP') | ||
| ->default(env('MIKROTIK_NAT_SYNC_IP')) | ||
| ->required(), | ||
| TextInput::make('mk_port') | ||
| ->label('REST API Port') | ||
| ->default(env('MIKROTIK_NAT_SYNC_PORT', '9080')) | ||
| ->required(), | ||
| TextInput::make('mk_user') | ||
| ->label('Username') | ||
| ->default(env('MIKROTIK_NAT_SYNC_USER')) | ||
| ->required(), | ||
| TextInput::make('mk_pass') | ||
| ->label('Password') | ||
| ->password() | ||
| ->revealable() | ||
| ->default(env('MIKROTIK_NAT_SYNC_PASSWORD')), | ||
| TextInput::make('mk_interface') | ||
| ->label('WAN Interface') | ||
| ->default(env('MIKROTIK_NAT_SYNC_INTERFACE', 'ether1')) | ||
| ->required(), | ||
| TextInput::make('mk_forbidden_ports') | ||
| ->label('Forbidden Ports (comma separated)') | ||
| ->placeholder('22, 80, 443, 3306') | ||
| ->default(env('MIKROTIK_NAT_SYNC_FORBIDDEN_PORTS')), | ||
| Select::make('mk_interval') | ||
| ->label('Sync Interval') | ||
| ->options([ | ||
| 'everyMinute' => 'Every Minute', | ||
| 'everyFiveMinutes' => 'Every 5 Minutes', | ||
| 'everyTenMinutes' => 'Every 10 Minutes', | ||
| 'hourly' => 'Hourly', | ||
| ]) | ||
| ->default(env('MIKROTIK_NAT_SYNC_INTERVAL', 'everyFiveMinutes')) | ||
| ->required(), | ||
| ]; | ||
| } | ||
|
|
||
| public function saveSettings(array $data): void | ||
| { | ||
| $this->writeToEnvironment([ | ||
| 'MIKROTIK_NAT_SYNC_IP' => $data['mk_ip'], | ||
| 'MIKROTIK_NAT_SYNC_PORT' => $data['mk_port'], | ||
| 'MIKROTIK_NAT_SYNC_USER' => $data['mk_user'], | ||
| 'MIKROTIK_NAT_SYNC_PASSWORD' => $data['mk_pass'], | ||
| 'MIKROTIK_NAT_SYNC_INTERFACE' => $data['mk_interface'], | ||
| 'MIKROTIK_NAT_SYNC_INTERVAL' => $data['mk_interval'], | ||
| 'MIKROTIK_NAT_SYNC_FORBIDDEN_PORTS' => $data['mk_forbidden_ports'], | ||
| ]); | ||
|
|
||
| Notification::make() | ||
| ->title('Settings saved successfully') | ||
| ->success() | ||
| ->send(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <?php | ||
|
|
||
| namespace Avalon\MikroTikNATSync\Providers; | ||
|
|
||
| use Illuminate\Support\ServiceProvider; | ||
|
|
||
| class MikroTikNATSyncPluginProvider extends ServiceProvider | ||
| { | ||
| public function register(): void | ||
| { | ||
| // | ||
| } | ||
|
|
||
| public function boot(): void | ||
| { | ||
| // | ||
| } | ||
|
Comment on lines
+7
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Command registration and scheduling should be moved here from the plugin class.
Proposed fix+use Avalon\MikroTikNATSync\Console\Commands\SyncMikrotikCommand;
+use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\ServiceProvider;
class MikroTikNATSyncPluginProvider extends ServiceProvider
{
public function register(): void
{
- //
+ $this->commands([
+ SyncMikrotikCommand::class,
+ ]);
}
public function boot(): void
{
- //
+ $this->callAfterResolving(Schedule::class, function (Schedule $schedule) {
+ $interval = env('MIKROTIK_SYNC_INTERVAL', 'everyFiveMinutes');
+ $schedule->command('mikrotik:sync')
+ ->{$interval}()
+ ->withoutOverlapping();
+ });
}
}🤖 Prompt for AI Agents |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add return type to
handle()— PHPStan failure across all PHP versions.PHPStan reports this on PHP 8.2–8.5. Laravel commands conventionally return
int(exit code).Also update
return;statements (Lines 30, 61) toreturn 1;for error paths, and addreturn 0;before the closing of the try block (after Line 95).🧰 Tools
🪛 GitHub Check: PHPStan (8.2)
[failure] 14-14:
Method Avalon\MikroTikNATSync\Console\Commands\SyncMikrotikCommand::handle() has no return type specified.
🪛 GitHub Check: PHPStan (8.3)
[failure] 14-14:
Method Avalon\MikroTikNATSync\Console\Commands\SyncMikrotikCommand::handle() has no return type specified.
🪛 GitHub Check: PHPStan (8.4)
[failure] 14-14:
Method Avalon\MikroTikNATSync\Console\Commands\SyncMikrotikCommand::handle() has no return type specified.
🪛 GitHub Check: PHPStan (8.5)
[failure] 14-14:
Method Avalon\MikroTikNATSync\Console\Commands\SyncMikrotikCommand::handle() has no return type specified.
🤖 Prompt for AI Agents