Skip to content

RADIUS Server Setup

ProxPanel ships its own RADIUS server — there is no FreeRADIUS underneath. The server is written in Go, lives in internal/radius/ (server.go, coa.go, ha.go), and runs in a dedicated container (proisp-radius) listening on UDP ports 1812 and 1813.

This page covers the ports it exposes, how to register a Network Access Server (NAS) so packets are accepted, the authentication methods supported, and the accounting interim-update mechanism that drives quota tracking.

PortDirectionPurposeNotes
UDP 1812NAS to ProxPanelAuthentication (Access-Request, Access-Accept, Access-Reject)Standard RFC-2865 port.
UDP 1813NAS to ProxPanelAccounting (Accounting-Request Start / Interim-Update / Stop)Standard RFC-2866 port.
UDP 3799 or 1700ProxPanel to NASChange-of-Authorization and Disconnect (RFC-3576 / 5176)3799 is RFC-default; 1700 is the MikroTik default — see CoA & Disconnect.

The first two ports listen on 0.0.0.0 inside the RADIUS container (docker-compose.yml uses network_mode: host so the container binds the host’s UDP sockets directly).

CoA is outbound — ProxPanel originates the packet to the NAS, not the other way around. The NAS only needs to allow inbound UDP on its CoA port from the ProxPanel server’s IP.

When a UDP packet arrives on 1812 or 1813, the server does this (server.go: GetSecret):

  1. Reads the source IP.
  2. Looks it up in the in-memory secrets map (loaded from nas_devices on startup and refreshed on demand).
  3. If found, uses the shared secret to decode and authenticate the RADIUS packet.
  4. If not found in standard mode, logs unknown NAS: x.x.x.x and drops the packet.

There is no global RADIUS secret in standard installations. Each NAS has its own. (SaaS mode runs an additional shared-secret path for tenant routers — that’s covered in Generic RADIUS and the SaaS docs.)

This is the single most common cause of “MikroTik says RADIUS timeout”:

A NAS must exist in the nas_devices table before any RADIUS packets from it will be accepted.

  1. In the panel, go to NAS / Routers in the sidebar and click Add NAS.
  2. Fill in:
    • Name — a human label, e.g. BNG-Beirut-01.
    • IP address — the interface IP the router will SOURCE RADIUS packets from. Not the router’s WAN IP, not the management IP — the actual outgoing IP for UDP 1812/1813 (/ip address print on RouterOS).
    • RADIUS secret — any string up to 100 characters. The same value goes in the router’s /radius add command.
    • CoA port — 1700 for MikroTik default, 3799 for RFC-default, or whatever you configured.
    • API username / password (MikroTik only) — for IP pool auto-import, torch, and dynamic queue management.
  3. Save. The radius container picks up the change within seconds (it polls nas_devices periodically), or you can restart it: docker restart proxpanel-radius.
  4. Verify the load: docker logs proxpanel-radius 2>&1 | grep "Loaded" — you should see Loaded N NAS secrets with N increased by 1.

Without this step the radius server log will fill with unknown NAS: x.x.x.x and your subscribers will not authenticate.

MethodWire formatRFCNotes
PAPCleartext password in User-Password (encrypted under the shared secret)RFC-2865Required for some non-MikroTik NAS. The subscriber’s password is stored encrypted (AES-256-GCM, license-derived key) and decrypted in memory at auth time.
CHAPCHAP-Password + CHAP-ChallengeRFC-1994Rarely used but accepted.
MS-CHAPv2MS-CHAP-Challenge + MS-CHAP2-Response (vendor MS attrs)RFC-2759The default for MikroTik PPPoE. ProxPanel implements the NT-hash dance natively in server.go so no external mschap_v2 library is needed.
EAP / TTLS / TLSRFC-3748Not currently supported. WPA2-Enterprise wireless is on the roadmap.

For MikroTik PPPoE, MS-CHAPv2 is what you want — leave the router’s PPP profile at its default. Plain PAP works but exposes the password to anyone who can sniff between the router and the panel, even though the channel is “encrypted” under the RADIUS shared secret (the encryption uses MD5 and is trivially broken with a known secret).

NAS ProxPanel RADIUS
| |
| Access-Request (User-Name + creds) ----> |
| | 1. Look up subscriber by username
| | 2. Decrypt stored password
| | 3. Validate (PAP / CHAP / MS-CHAPv2)
| | 4. Check is_active, expiry_date
| | 5. Check MAC binding (if set)
| | 6. Build reply: Mikrotik-Rate-Limit,
| | Framed-IP-Address (if assigned),
| | Framed-Pool (if Service has one),
| | Acct-Interim-Interval
| <---- Access-Accept + attributes |
| |

The reply attributes are assembled per subscriber. The most common ones:

AttributeSourceEffect on MikroTik
Mikrotik-Rate-LimitService’s download_speed_str / upload_speed_str (normalized to 1200k/2000k)Creates the dynamic simple queue.
Framed-IP-AddressSubscriber’s static IP or radreply entry or pool-allocated IPBecomes the user’s PPPoE IP.
Framed-PoolService’s pool_name (if set, no Framed-IP-Address)Tells MikroTik to assign from this pool.
Acct-Interim-Intervalradius_interim_update_seconds system preference (default 30 s)How often the router sends Interim-Update accounting.
Idle-TimeoutService’s idle_timeout if setDisconnect after N seconds of idle.
Session-TimeoutComputed from expiry_date if hard-cut enabledForce disconnect at end of plan.

The accounting socket (UDP 1813) receives:

  • Acct-Start — when the PPPoE session comes up. Creates a row in radacct.
  • Acct-Interim-Update — every N seconds (default 30) while the session is up. Updates radacct.acctupdatetime and bytes counters.
  • Acct-Stop — when the session ends. Sets radacct.acctstoptime and the final byte counters.

Interim updates are what makes the Stale Session Cleanup and the dashboard’s online-count work — a session with no interim update for more than 30 minutes is presumed dead and closed automatically.

Every accepted or rejected RADIUS event is logged to the radius_logs table. To avoid spawning a goroutine per packet (which would OOM the box at 30 K subscribers), the server uses a fixed 8-worker pool draining a 4096-event channel (server.go: initRadiusLogWorkers). If the queue is full — your DB is slow — events are dropped, not blocked. A drop counter (radiusLogDropped) increments so you can spot the problem.

This means radius_logs is a best-effort observability surface, not a billing log. Bytes are billed from radacct.

  1. docker logs proxpanel-radius 2>&1 | grep -i "unknown NAS" — if you see your router’s source IP here, register it.
  2. Confirm the secret matches — RouterOS stores it masked; if you’re not sure, retype it on both sides.
  3. Confirm the router is sourcing from the IP you registered: on RouterOS, /tool sniffer quick interface=ether1 port=1812. The first packet shows the source IP.
  4. Confirm UDP 1812/1813 reach the panel server: nc -uvz <panel-ip> 1812 from the router shell, or tcpdump -ni any port 1812 on the panel.
  1. Confirm accounting is also enabled on the router. On MikroTik: /radius print detail must show accounting=yes.
  2. Confirm accounting packets reach the panel: tcpdump -ni any port 1813.
  3. Look in radacct for the username: SELECT * FROM radacct WHERE username='...' ORDER BY acctstarttime DESC LIMIT 5; — if rows exist but acctupdatetime doesn’t advance, the router is sending Start/Stop but not Interim — check the router’s accounting interval.