DHCP is fine until you need to bill people. The day a subscriber swaps their router, disputes their usage, or you need to throttle a plan across twelve towers, PPPoE with FreeRADIUS is the stack that holds up.
Here is the working shape of it, end to end.
Architecture#
Subscriber CPE ──PPPoE──> MikroTik BNG ──RADIUS──> FreeRADIUS ──SQL──> PostgreSQL│ │rate limit accounting(dynamic queue) (billing source)
RouterOS is the BNG (broadband network gateway). It terminates PPPoE sessions and asks FreeRADIUS a single question: may this user in, and at what speed?
1. PPPoE server on RouterOS#
/ip pooladd name=pppoe-pool ranges=10.50.0.2-10.50.255.254/ppp profileadd name=pppoe-default local-address=10.50.0.1 remote-address=pppoe-pool \dns-server=10.50.0.1 use-encryption=no only-one=yes/interface pppoe-server serveradd service-name=isp interface=bridge-access default-profile=pppoe-default \authentication=pap,chap one-session-per-host=yes disabled=no
only-one=yes and one-session-per-host=yes together stop credential sharing between households — the single highest-value line in this file.
2. Point RouterOS at RADIUS#
/radiusadd service=ppp address=10.60.0.5 secret=<strong-shared-secret> \timeout=800ms src-address=10.60.0.1/ppp aaaset use-radius=yes accounting=yes interim-update=5m/radius incomingset accept=yes port=3799
interim-update=5m is the sweet spot. Shorter floods the RADIUS server on a large base; longer and a mid-session disconnect loses too much usage data to bill fairly.
3. FreeRADIUS with SQL#
Enable the sql module and store plans as reply attributes rather than hard-coded queues:
-- Plan: Home 20 Mbps, burst to 25 for 15sINSERT INTO radgroupreply (groupname, attribute, op, value) VALUES('home-20', 'Mikrotik-Rate-Limit', ':=', '10M/20M 12M/25M 10M/20M 15');-- Assign a subscriber to a planINSERT INTO radusercheck (username, attribute, op, value) VALUES('ali.k', 'Cleartext-Password', ':=', 'change-me');INSERT INTO radusergroup (username, groupname, priority) VALUES('ali.k', 'home-20', 1);
The Mikrotik-Rate-Limit syntax is rx/tx burst-rx/burst-tx threshold-rx/threshold-tx burst-time. Direction is from the router's point of view, so the first value is the subscriber's upload. Get this backwards and every customer reports "my download is only 10 Mbps" — I have made that call myself.
4. Suspend and restore without touching the router#
Non-payment handling should never mean logging into a RouterOS box. Move the subscriber to a restricted group and send a disconnect:
echo "User-Name=ali.k,Acct-Session-Id=81a0043c,NAS-IP-Address=10.60.0.1" \| radclient -x 10.60.0.1:3799 disconnect <shared-secret>
Next reconnect they land in the suspended group, whose reply attributes point DNS at a payment-reminder page and cap the rate at 512k. Pay, group flips back, session resets, service restored. No truck roll.
5. Accounting you can actually bill from#
radacct gives you start time, stop time, input and output octets per session. Two practical notes:
- Use
BIGINTcolumns. 32-bit octet counters wrap at 4 GB and a single evening of streaming will corrupt an invoice. - Reconcile against interface counters weekly. Sessions that end in a power cut close with a stale stop record; interim updates are what save you.
Sizing and failure modes#
| Symptom | Usual cause |
|---|---|
| Random session drops at the same time daily | RADIUS timeout too tight, or DB backup locking radacct |
| Speeds ignore the plan | Rate-limit direction reversed, or a static queue overriding the dynamic one |
| Sessions stick after CPE reboot | No only-one=yes; stale session holds the username |
| CPU spikes on the BNG | PPPoE encryption enabled unnecessarily, or too many interim updates |
An RB5009 comfortably terminates a few hundred PPPoE sessions. Past roughly a thousand, move to a CCR-class BNG and split by area.
Where this goes next#
Once authentication and accounting are solid, the missing piece is the business layer: plans, invoices, vouchers, and a support view of who is online right now. That is precisely what NexaLink puts on top of this stack — the same FreeRADIUS schema, with an operator UI over it.



