Skip to content

15 — MikroTik Integration

Overview

PyroRadius integrates with MikroTik routers via two mechanisms: the RouterOS API (primary) and SNMP (fallback). The RouterOS API enables live traffic reading, session management, and active PPPoE session counting. SNMP is used when the API is unavailable or for non-MikroTik NAS devices. Two service classes handle this: MikroTikService (live traffic + session kick) and RouterOsService (online session count).


RouterOS API vs SNMP

Capability RouterOS API (port 8728) SNMP IF-MIB
Live traffic (per interface) Yes — fast, accurate Yes — fallback, requires 2 polls
Active PPPoE session count Yes — /ppp/active/print No
Session disconnect (kick) Yes — /ppp/active/remove No
Rate limit / queue management Yes No
Interface list Yes Yes
Authentication required Yes (username/password) No (community string)
Requires open port 8728 (TCP) 161 (UDP)

Decision logic: Always attempt RouterOS API first. If the connection fails (timeout, refused, wrong credentials), fall back to SNMP for traffic data. Session kick and online count have no SNMP fallback — they require the API.


RouterOS API Library

Package: evilfreelancer/routeros-api-php

Installed via Composer:

composer require evilfreelancer/routeros-api-php

Connection Settings

use RouterOS\Client;
use RouterOS\Config;

$config = new Config([
    'host'     => $nas->nasname,        // NAS IP address
    'user'     => $nas->api_user,
    'pass'     => decrypt($nas->api_password),
    'port'     => $nas->api_port ?? 8728,
    'timeout'  => 3,                    // 3 second connection timeout
    'attempts' => 1,                    // No retry on connection failure
    'delay'    => 0,
]);

$client = new Client($config);

The short timeout (3s) and single attempt prevent UI hangs when a NAS is unreachable. The caller catches \RouterOS\Exceptions\ClientException and proceeds to SNMP fallback.


MikroTikService — Live Traffic

Method: liveTraffic(Nas $nas, string $interface): array

Returns current download and upload rates in bytes per second for a specific interface on a NAS.

Full Logic Flow

1. Try RouterOS API connection
   ├─ SUCCESS → query /interface/monitor-traffic
   │     ├─ Send: /interface/monitor-traffic with interface=<name>, once=yes
   │     ├─ Receive: rx-bits-per-second, tx-bits-per-second
   │     └─ Return: {download: rx/8, upload: tx/8}  (bits→bytes)
   └─ FAILURE (timeout / refused) → fall back to SNMP
         ├─ SNMP Walk: IF-MIB::ifDescr  →  build ifIndex map {name → index}
         ├─ Find index for requested interface name
         ├─ Reading 1: fetch ifHCInOctets[index], ifHCOutOctets[index]
         ├─ Sleep 800ms
         ├─ Reading 2: fetch ifHCInOctets[index], ifHCOutOctets[index]
         ├─ Delta calculation:
         │     in_delta  = reading2.in  - reading1.in
         │     out_delta = reading2.out - reading1.out
         │   Handle counter wrap (32-bit counters on older devices):
         │     if (delta < 0): delta = (MAX_UINT32 - reading1) + reading2 + 1
         │   For 64-bit (ifHC*): wrap at 2^64 - 1 (extremely rare)
         └─ Return: {download: in_delta/0.8, upload: out_delta/0.8}
               (divide by 0.8 seconds = convert per-800ms to per-second)

Why Two Readings for SNMP?

SNMP counters are cumulative totals, not rates. To derive a rate, you need two readings and the time delta between them. The 800ms interval is a trade-off: long enough to get meaningful delta values, short enough to feel responsive in the UI.

Counter Wrap Handling

MikroTik devices report 64-bit counters (IF-MIB::ifHCInOctets). At very high throughput, even 64-bit counters can theoretically wrap. The wrap detection logic:

if ($delta < 0) {
    // 32-bit wrap:
    $delta = (PHP_INT_MAX & 0xFFFFFFFF) - $reading1 + $reading2 + 1;
    // 64-bit wrap: same logic but with 2^64 - 1 as max
}

In practice, 64-bit counter wrap takes hundreds of years at 10Gbps; 32-bit counters wrap in minutes at high speeds, which is why ifHC* (64-bit) OIDs are preferred over if* (32-bit) OIDs.


RouterOsService — Active PPPoE Count

Method: totalActivePPPoE(Nas $nas): int

Returns the number of currently active PPPoE sessions on a NAS. Used for the "Online Now" metric on the dashboard.

Logic

$query = new Query('/ppp/active/print');
$query->add('?type=pppoe');       // filter to PPPoE sessions only
$response = $client->query($query)->read();
return count($response);

The RouterOS API returns one array element per active session. Counting elements gives the session count.

Error Handling

If the API connection fails: - Returns 0 (safe default). - Logs the error to laravel.log. - The dashboard shows "0" rather than crashing.

This is the same count that feeds the "ONLINE NOW" widget on the admin dashboard. See project memory (PyroRadius Online Count) for the full history of fixes to this metric.


Session Disconnect — Kick Command

Route

POST /customers/{id}/kick
Handled by CustomerController::kick()

Logic

$nas = $customer->nas;
$client = MikroTikService::connect($nas);

// Find active session by username
$query = new Query('/ppp/active/print');
$query->add('?name=' . $customer->pppoe_username);
$sessions = $client->query($query)->read();

foreach ($sessions as $session) {
    $removeQuery = new Query('/ppp/active/remove');
    $removeQuery->add('=.id=' . $session['.id']);
    $client->query($removeQuery)->read();
}

When executed: 1. RouterOS removes the active PPPoE session. 2. The customer's router receives a disconnect event. 3. The router typically auto-redials within seconds. 4. The new session goes through RADIUS auth again — if the account is expired or suspended, auth is rejected.

Kick is used to force immediate enforcement of status changes (e.g., after suspending a customer who has an active session).


Rate Limiting via RADIUS Groups

Speed limits are enforced by FreeRADIUS returning the Mikrotik-Rate-Limit attribute in the Access-Accept response. This attribute is stored in radgroupreply:

groupname  = "10Mbps-Package"
attribute  = "Mikrotik-Rate-Limit"
op         = ":="
value      = "10M/5M 15M/8M 8M/5M 8M/5M 30"

Mikrotik-Rate-Limit Attribute Format

"<download>/<upload> <burst_download>/<burst_upload> <burst_threshold_download>/<burst_threshold_upload> <burst_time>"

Five space/slash-separated values:

Position Field Example Description
1 download 10M Normal download rate limit
2 upload 5M Normal upload rate limit
3 burst_download 15M Max download during burst
4 burst_upload 8M Max upload during burst
5 burst_threshold_download 8M When download drops to this, burst activates
6 burst_threshold_upload 5M When upload drops to this, burst activates
7 burst_time 30 How long (seconds) burst can sustain

Full example: "10M/5M 15M/8M 8M/5M 8M/5M 30"

How Burst Works on MikroTik

Burst allows customers to temporarily exceed their normal rate. Example with 10M/5M 15M/8M 8M/5M 8M/5M 30: - Normal speed: 10 Mbps down / 5 Mbps up. - If actual usage drops below 8 Mbps down (burst threshold), the router "saves" burst credit. - When the customer starts a new download, they get up to 15 Mbps for up to 30 seconds. - After 30 seconds, speed drops back to 10 Mbps.

Package Burst Fields in PyroRadius

Packages table stores these fields separately: - speed_download — normal download (e.g. 10) - speed_upload — normal upload (e.g. 5) - burst_download — burst download max - burst_upload — burst upload max - burst_threshold_download — threshold to enable burst - burst_threshold_upload - burst_time — seconds - speed_unit — M or K (Mbps or Kbps)

When a customer is added to a RADIUS group, the Mikrotik-Rate-Limit value is assembled from these fields:

$rateLimit = "{$pkg->speed_download}{$unit}/{$pkg->speed_upload}{$unit} "
           . "{$pkg->burst_download}{$unit}/{$pkg->burst_upload}{$unit} "
           . "{$pkg->burst_threshold_download}{$unit}/{$pkg->burst_threshold_upload}{$unit} "
           . "{$pkg->burst_time}";


PollTraffic Command

php artisan nas:poll-traffic

Iterates all active NAS devices and calls MikroTikService::liveTraffic() for each monitored interface. Stores readings in nas_port_readings for historical charts.

Scheduled every minute via Laravel scheduler:

$schedule->command('nas:poll-traffic')->everyMinute();


RouterOS API Queries Reference

Common RouterOS API queries used in PyroRadius:

List Active PPPoE Sessions

/ppp/active/print
?type=pppoe

Get Session by Username

/ppp/active/print
?name=customer_pppoe_username

Remove Session (Kick)

/ppp/active/remove
=.id=*1A

Monitor Interface Traffic (Real-Time)

/interface/monitor-traffic
=interface=pppoe-out1
=once=yes
Returns: rx-bits-per-second, tx-bits-per-second

Get Interface List

/interface/print

Get Router Version

/system/resource/print
Returns RouterOS version — used by the "Test Connection" feature.


MikroTikService Class Structure

class MikroTikService
{
    public static function connect(Nas $nas): Client
    // Builds and returns RouterOS API client with NAS credentials

    public static function liveTraffic(Nas $nas, string $interface): array
    // Returns ['download' => bytes/s, 'upload' => bytes/s]
    // Tries API first, falls back to SNMP

    public static function kickSession(Nas $nas, string $pppoeUsername): bool
    // Disconnects active session; returns true if session found and removed

    public static function getActiveSessions(Nas $nas): array
    // Returns all active PPPoE sessions on the NAS

    private static function snmpTraffic(Nas $nas, string $interface): array
    // SNMP fallback: two readings 800ms apart, delta calculation

    private static function findIfIndex(Nas $nas, string $interface): ?int
    // SNMP walk IF-MIB::ifDescr to find the ifIndex for a named interface
}

RouterOsService Class Structure

class RouterOsService
{
    public static function totalActivePPPoE(Nas $nas): int
    // Counts active PPPoE sessions via /ppp/active/print?type=pppoe

    public static function getOnlineCount(): int
    // Aggregates totalActivePPPoE across ALL active NAS devices
    // Used for dashboard "ONLINE NOW" metric
}

Error Handling and Timeouts

Error Handling
Connection refused (port 8728 closed) Catch exception → SNMP fallback → log warning
Authentication failure Catch exception → SNMP fallback → log error
Timeout (>3 seconds) Catch exception → SNMP fallback
SNMP community mismatch SNMP returns empty → return
No active session found (kick) Return false → UI shows "No active session" message
NAS unreachable during PollTraffic Skip NAS → continue loop → log warning

All exceptions are caught per-NAS so one unreachable router does not crash the entire poll loop.


Security Notes

  • api_password is encrypted with Laravel's Crypt::encrypt() before storage and decrypted in-memory only when building the RouterOS API connection.
  • The RouterOS API user should have minimum required permissions (api, read, write — not full admin).
  • API port 8728 should be restricted on the MikroTik firewall to only accept connections from the PyroRadius application server IP.
  • SNMP community string should be changed from the default public to a private string and restricted by source IP on the NAS.