Skip to content

Router + RADIUS (MikroTik) — Developer Notes (IspBills / main) + Code Examples #165

@lupael

Description

@lupael

Please check following files from another ISP billing system. Those files are for your study purpose . Please check how other platform using various features, take concept from them , start implement now but dont break our roles. Dont forget about UI devlopment.

Router + RADIUS (MikroTik) — Developer Notes (IspBills / main) + Code Examples

This doc explains how a Admin adds/configures a router, how import/sync works for IP pools / PPP profiles / PPP secrets, how Router ↔ FreeRADIUS responsibilities are split, how to choose Router vs RADIUS authentication, and how backups work. It also includes code examples aligned with the repository patterns.

Code search results are limited to 10 hits; browse more:


1) Router vs RADIUS (Responsibilities)

MikroTik Router

  • PPP endpoint (PPPoE), session enforcement (profile, pool, firewall).
  • Can authenticate locally (/ppp/secret) or via RADIUS.

FreeRADIUS

  • Central AAA (auth, authorization attributes like Framed-IP-Address, accounting).

In IspBills, the router is configured to use Radius (/ppp/aaa use-radius=yes) but the app can maintain local secrets for fallback or Router-auth mode.


2) Admin adds Router (NAS)

Relevant files

  • Controller: app/Http/Controllers/Freeradius/NasController.php
  • Views:
    • resources/views/admins/group_admin/routers-create.blade.php
    • resources/views/admins/components/routers-create.blade.php

Example: RouterOS API connectivity check (pattern used in repo)

<?php

use RouterOS\Sohag\RouterosAPI;

$config = [
    'host' => $request->nasname,
    'user' => $request->api_username,
    'pass' => $request->api_password,
    'port' => $request->api_port,
    'attempts' => 1,
    'debug' => false,
];

$api = new RouterosAPI($config);

if (!$api->connect($config['host'], $config['user'], $config['pass'])) {
    return redirect()->route('routers.create')
        ->with('error', 'Can not connect to the router! Check API port, username, password or port forwarding');
}

This mirrors what NasController@store does before saving a router.


3) Configure Router (push config to MikroTik)

Relevant file

  • app/Http/Controllers/RouterConfigurationController.php

Example: Add RADIUS client on router (repo pattern)

$menu = 'radius';

$rows = [
    [
        'accounting-port' => 3613,
        'address' => $radius_server,      // from config(database.connections.<radius_db_connection>.public_ip)
        'authentication-port' => 3612,
        'secret' => $router['secret'],    // NAS shared secret
        'service' => 'hotspot,ppp',
        'timeout' => '3s',
        'require-message-auth' => 'no',
    ]
];

// Replace existing radius rows with desired one
$router_rows = $api->getMktRows($menu);
$api->removeMktRows($menu, $router_rows);
$api->addMktRows($menu, $rows);

Example: Enable PPP AAA to use RADIUS + accounting (repo uses ttyWirte)

$api->ttyWirte('/ppp/aaa/set', [
    'interim-update' => '5m',
    'use-radius' => 'yes',
    'accounting' => 'yes',
]);

Example: Enable radius incoming

$api->ttyWirte('/radius/incoming/set', [
    'accept' => 'yes',
]);

Example: Update PPP profiles (set local-address / scripts)

Repo pattern is:

  • fetch ppp_profile rows
  • loop and call editMktRow('ppp_profile', $row, $payload)
$local_address = ['local-address' => '10.0.0.1'];

$ppp_profiles = $api->getMktRows('ppp_profile', ['default' => 'yes']);
while ($ppp_profile = array_shift($ppp_profiles)) {
    $api->editMktRow('ppp_profile', $ppp_profile, $local_address);
}

4) Import from MikroTik → MySQL (IP Pool, PPP Profile, PPP Secret)

Relevant file

  • app/Http/Controllers/Mikrotik/MikrotikDbSyncController.php

Example: Import IP pools (router → DB)

// delete old imported pools for this router+mgid
mikrotik_ip_pool::where([
    ['nas_id', '=', $router->id],
    ['mgid', '=', $group_admin->mgid],
])->delete();

// fetch from router
$ip4pools = $api->getMktRows('ip_pool');

while ($ip4pool = array_shift($ip4pools)) {
    $ranges = self::parseIpPool($ip4pool['ranges']);
    if ($ranges == 0) {
        continue;
    }

    $ip_pool = new mikrotik_ip_pool();
    $ip_pool->customer_import_request_id = $customer_import_request->id;
    $ip_pool->mgid = $customer_import_request->mgid;
    $ip_pool->operator_id = $customer_import_request->operator_id;
    $ip_pool->nas_id = $customer_import_request->nas_id;
    $ip_pool->name = $ip4pool['name'] ?? 'null';
    $ip_pool->ranges = $ranges;
    $ip_pool->save();
}

Example: Import PPP profiles (router → DB)

mikrotik_ppp_profile::where([
    ['nas_id', '=', $router->id],
    ['mgid', '=', $group_admin->mgid],
])->delete();

$ppp_profiles = $api->getMktRows('ppp_profile', ['default' => 'no']);

while ($ppp_profile = array_shift($ppp_profiles)) {
    $row = new mikrotik_ppp_profile();
    $row->customer_import_request_id = $customer_import_request->id;
    $row->mgid = $customer_import_request->mgid;
    $row->operator_id = $customer_import_request->operator_id;
    $row->nas_id = $customer_import_request->nas_id;

    $row->name = $ppp_profile['name'] ?? 'Not Found';
    $row->local_address = $ppp_profile['local-address'] ?? '';
    $row->remote_address = $ppp_profile['remote-address'] ?? '';
    $row->save();
}

Example: Backup + Import PPP secrets (router → DB)

Repo creates a router-side export file before reading secrets:

// router-side export backup before import
$file = 'ppp-secret-backup-by-billing' . \Carbon\Carbon::now()->timestamp;
$api->ttyWirte('/ppp/secret/export', ['file' => $file]);

mikrotik_ppp_secret::where([
    ['nas_id', '=', $router->id],
    ['mgid', '=', $group_admin->mgid],
])->delete();

$query = ($customer_import_request->import_disabled_user == 'no')
    ? ['disabled' => 'no']
    : [];

$secrets = $api->getMktRows('ppp_secret', $query);

while ($secret = array_shift($secrets)) {
    $row = new mikrotik_ppp_secret();
    $row->customer_import_request_id = $customer_import_request->id;
    $row->mgid = $customer_import_request->mgid;
    $row->operator_id = $customer_import_request->operator_id;
    $row->nas_id = $customer_import_request->nas_id;

    $row->name = $secret['name'] ?? '';
    $row->password = $secret['password'] ?? '';
    $row->profile = $secret['profile'] ?? '';
    $row->remote_address = $secret['remote-address'] ?? null;
    $row->disabled = $secret['disabled'] ?? 'no';
    $row->comment = $secret['comment'] ?? null; // if present
    $row->save();
}

5) Sync / Provisioning to Router (PPP profile + secret)

5.1 Ensure PPP profile exists before creating secrets (repo dependency pattern)

From CustomersRadPasswordController:

$router_rows = $api->getMktRows('ppp_profile', [
    'name' => $package->master_package->pppoe_profile->name
]);

if (!count($router_rows)) {
    // push dependency
    PppProfilePushController::store($package->master_package->pppoe_profile, $router);
}

5.2 Create or update PPP secret (Router-auth mode)

// update if exists
$exist_rows = $api->getMktRows('ppp_secret', ['name' => $customer->username]);

if (count($exist_rows)) {
    $exist_row = array_shift($exist_rows);
    $api->editMktRow('ppp_secret', $exist_row, ['password' => $customer->password]);
} else {
    // create
    $ppp_secret = [
        'name' => $customer->username,
        'password' => $customer->password,
        'profile' => $package->master_package->pppoe_profile->name,
        'disabled' => 'no',
    ];

    $api->addMktRows('ppp_secret', [$ppp_secret]);
}

5.3 Static IP behavior (remote-address)

Repo sets remote-address when profile allocation mode is static (see PrimaryAuthenticatorChangeJob):

$ppp_secret = [
    'name' => $customer->username,
    'password' => $customer->password,
    'profile' => $pppoe_profile->name,
];

if ($pppoe_profile->ip_allocation_mode === 'static') {
    $ppp_secret['remote-address'] = $customer->login_ip;
}

6) Comments (customer metadata embedded into router objects)

Relevant file

  • app/Http/Controllers/Customer/CustomerBackupController.php

Example: comment builder used by backup/push

public static function getComment($customer): string
{
    return "oid--$customer->operator_id," .
        "zid--$customer->zone_id," .
        "name--$customer->name," .
        "mobile--$customer->mobile," .
        "bpid--$customer->billing_profile_id," .
        "exp_date--$customer->package_expired_at," .
        "ps--$customer->payment_status," .
        "status--$customer->status";
}

Example: applying comment to a PPP secret (recommended repo-style)

When pushing secrets (backup/sync), attach:

$ppp_secret['comment'] = CustomerBackupController::getComment($customer);

7) Authentication Choice: Router vs Radius (+ automatic fallback)

Primary switch

Controlled by backup_setting.primary_authenticator:

  • Radius: router authenticates via RADIUS; local secrets often disabled.
  • Router: router authenticates locally; app maintains /ppp/secret.

Netwatch fallback automation (Radius health monitoring)

Controller: app/Http/Controllers/NasNetWatchController.php

$menu = 'tool_netwatch';

$rows = [[
    'host' => $radius_server,
    'interval' => '1m',
    'timeout' => '1s',
    'up-script' => "/ppp secret disable [find disabled=no];/ppp active remove [find radius=no];",
    'down-script' => "/ppp secret enable [find disabled=yes];",
    'comment' => 'radius',
]];

// replace any existing netwatch for that host
$router_rows = $api->getMktRows($menu, ['host' => $radius_server]);
$api->removeMktRows($menu, $router_rows);
$api->addMktRows($menu, $rows);

Interpretation:

  • Radius UP => force Radius auth (disable local secrets; drop non-radius sessions)
  • Radius DOWN => enable local secrets (fallback)

8) Backups (with code examples)

8.1 Router-side PPP secret export (during import)

$file = 'ppp-secret-backup-by-billing' . \Carbon\Carbon::now()->timestamp;
$api->ttyWirte('/ppp/secret/export', ['file' => $file]);

8.2 Customer “backup/mirror” to router (PPPoE / Hotspot)

Controller: CustomerBackupController

  • PPPoE copy runs only when primary_authenticator !== 'Radius' check passes in repo logic (repo has conditional branches; verify per flow).

Typical pattern inside backup:

$exist_rows = $api->getMktRows('ppp_secret', ["name" => $ppp_secret['name']]);

if (count($exist_rows)) {
    $exist_row = array_shift($exist_rows);
    $api->editMktRow('ppp_secret', $exist_row, $ppp_secret);
} else {
    $api->addMktRows('ppp_secret', [$ppp_secret]);
}

8.3 App/server backup (Spatie)

Configured in:

  • config/backup.php

This is separate from router backups and handles filesystem/database backups.


9) Minimal “End-to-End” example: Configure router for RADIUS + ensure fallback

// 1) Configure radius client + PPP AAA
$api->addMktRows('radius', [[
    'accounting-port' => 3613,
    'address' => $radius_server,
    'authentication-port' => 3612,
    'secret' => $nas->secret,
    'service' => 'hotspot,ppp',
    'timeout' => '3s',
    'require-message-auth' => 'no',
]]);
$api->ttyWirte('/ppp/aaa/set', [
    'interim-update' => '5m',
    'use-radius' => 'yes',
    'accounting' => 'yes',
]);
$api->ttyWirte('/radius/incoming/set', ['accept' => 'yes']);

// 2) Add netwatch for radius up/down behavior
$api->addMktRows('tool_netwatch', [[
    'host' => $radius_server,
    'interval' => '1m',
    'timeout' => '1s',
    'up-script' => "/ppp secret disable [find disabled=no];/ppp active remove [find radius=no];",
    'down-script' => "/ppp secret enable [find disabled=yes];",
    'comment' => 'radius',
]]);

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions