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:
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¶
Handled byCustomerController::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:
RouterOS API Queries Reference¶
Common RouterOS API queries used in PyroRadius:
List Active PPPoE Sessions¶
Get Session by Username¶
Remove Session (Kick)¶
Monitor Interface Traffic (Real-Time)¶
Returns:rx-bits-per-second, tx-bits-per-second
Get Interface List¶
Get Router Version¶
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_passwordis encrypted with Laravel'sCrypt::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
publicto a private string and restricted by source IP on the NAS.