Skip to content
Closed
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ npm i -g yarn
- [Legal Pages](/legal-pages) - Adds legal pages (Imprint, Privacy Policy, ToS) to the panel
- [McLogCleaner](/mclogcleaner) - Delete old logs with ease
- [MCLogs Uploader](/mclogs-uploader) - Upload console logs to mclo.gs
- [MikroTik NAT Sync](/mikrotik-nat-sync) - Automatically synchronize Pelican allocations with MikroTik NAT rules via REST API.
- [Minecraft Modrinth](/minecraft-modrinth) - Download Minecraft mods & plugins from Modrinth
- [PasteFox Share](/pastefox-share) - Share console logs via pastefox.com
- [Player Counter](/player-counter) - Show connected players count for game servers
Expand Down
34 changes: 34 additions & 0 deletions mikrotik-nat-sync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

# 🌐 MikroTik NAT Sync for Pelican Panel

![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Platform](https://img.shields.io/badge/platform-Pelican%20Panel-orange.svg)
![AI](https://img.shields.io/badge/Created%20with-AI%20Gemini-brightgreen.svg)

**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)
5 changes: 5 additions & 0 deletions mikrotik-nat-sync/config/mikrotik-nat-sync.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

return [
// Config values for MikroTik NAT Sync
];
15 changes: 15 additions & 0 deletions mikrotik-nat-sync/plugin.json
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
}
119 changes: 119 additions & 0 deletions mikrotik-nat-sync/src/Console/Commands/SyncMikrotikCommand.php
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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).

-    public function handle()
+    public function handle(): int

Also update return; statements (Lines 30, 61) to return 1; for error paths, and add return 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
In `@mikrotik-nat-sync/src/Console/Commands/SyncMikrotikCommand.php` at line 14,
Update the command's handle method signature to declare an int return type
(change function handle() to function handle(): int) and replace any bare
"return;" used for error exits with "return 1;" (these are the early error
returns in the method), and add "return 0;" at the end of the successful try
block just before it closes so the method returns 0 on success; specifically
modify the handle method and its existing bare return statements to follow
Laravel's int exit-code convention.

{
$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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find migration files related to servers table
find . -type f -name "*migration*" -o -name "*migrations*" | head -20

Repository: pelican-dev/plugins

Length of output: 318


🏁 Script executed:

#!/bin/bash
# Search for server table migrations
fd -t f "migration" | grep -i server | head -10

Repository: 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 -10

Repository: 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 -80

Repository: 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 -100

Repository: 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 -20

Repository: 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/null

Repository: 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 -10

Repository: 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 -20

Repository: 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 -20

Repository: 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 -10

Repository: 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 -20

Repository: 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 -30

Repository: 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.php

Repository: 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 -50

Repository: 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 -80

Repository: 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 -30

Repository: 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 -10

Repository: 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 -40

Repository: 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 -20

Repository: 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 -20

Repository: 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 -20

Repository: 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 -30

Repository: 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 3

Repository: 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 -20

Repository: pelican-dev/plugins

Length of output: 45


🌐 Web query:

Pelican Panel server model database schema columns

💡 Result:

In Pelican Panel, the App\Models\Server model’s documented core database columns (for the servers table) are: id, uuid, uuid_short, external_id, owner_id, node_id, egg_id, allocation_id, name, description, status, memory, swap, disk, cpu, threads, io, oom_killer, startup, image, skip_scripts, database_limit, allocation_limit, backup_limit, docker_labels, installed_at. [1]

Source: Pelican Panel “Server Model” (DeepWiki) [1]


Query fetches all servers regardless of installation or suspension status.

The DB query joins servers with allocations without filtering by server status. Uninstalled or suspended servers will generate NAT rules. The servers table in Pelican has a status column and an installed_at timestamp to identify active servers—use these to filter:

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
In `@mikrotik-nat-sync/src/Console/Commands/SyncMikrotikCommand.php` around lines
39 - 42, The query building $active_servers currently selects servers joined
with allocations without filtering by installation or suspension state; update
the DB::table('servers')->join('allocations', ...) query to only include
active/installed servers by adding conditions on the servers table such as
->where('servers.status', 'installed') (or the project's active status value)
and ->whereNotNull('servers.installed_at') so suspended/uninstalled servers are
excluded from NAT rule generation.


$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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Hardcoded private IP 192.168.70.231 will break any non-dev deployment.

When a server allocation has IP 0.0.0.0, the command falls back to a hardcoded internal address. This should be a configurable env var (e.g., MIKROTIK_DEFAULT_TARGET_IP), or the command should skip/warn instead.

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
In `@mikrotik-nat-sync/src/Console/Commands/SyncMikrotikCommand.php` at line 52,
Replace the hardcoded fallback IP by reading a configurable env/config value and
handling missing config: in SyncMikrotikCommand (handle method) stop using the
literal '192.168.70.231' for $target_ip when $srv->ip == '0.0.0.0'; instead read
an environment/config key (e.g., MIKROTIK_DEFAULT_TARGET_IP via env() or
config()), use that as the fallback if present, and if it is not set either skip
processing this $srv (with a warning log) or throw/exit with a clear error;
update references to $target_ip and $srv->ip accordingly so deployments can
control the default IP.

$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());
}
}
}
104 changes: 104 additions & 0 deletions mikrotik-nat-sync/src/MikroTikNATSyncPlugin.php
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();
}
}
18 changes: 18 additions & 0 deletions mikrotik-nat-sync/src/Providers/MikroTikNATSyncPluginProvider.php
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Command registration and scheduling should be moved here from the plugin class.

MikroTikNATSyncPlugin::boot() calls $this->commands(...), but commands() is defined on Illuminate\Support\ServiceProvider, not on FilamentPlugin. This causes the PHPStan failure across all PHP versions. Since this provider does extend ServiceProvider, it's the correct place for command registration and task scheduling.

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
In `@mikrotik-nat-sync/src/Providers/MikroTikNATSyncPluginProvider.php` around
lines 7 - 17, MikroTikNATSyncPlugin::boot() is calling commands() and scheduling
tasks but commands() belongs on a ServiceProvider; move all command registration
and task scheduling logic from MikroTikNATSyncPlugin::boot into this provider
(MikroTikNATSyncPluginProvider) — register console commands via
$this->commands([...]) in register() or boot(), and register the schedule using
Laravel's scheduling hook (e.g. $this->app->booted or
callAfterResolving(Schedule::class, ...)) inside
MikroTikNATSyncPluginProvider::boot(); update references to the original command
class names and the scheduling closure so PHPStan no longer sees commands()
called on FilamentPlugin.

}
Loading