CoA and Disconnect
Change of Authorization (CoA) and Disconnect-Request are the two RFC-3576 / 5176 packet types that let a RADIUS server change a session that is already up. ProxPanel uses them constantly: every speed plan change, every FUP tier transition, every “kick this subscriber” action from the operator flows through CoA.
This page explains the wire format, the Session-Id formatting that breaks more deployments than any other detail, the radclient and RouterOS API fallbacks, and why a CoA-NAK sometimes never arrives at all.
What CoA does
Section titled “What CoA does”A CoA-Request is a RADIUS packet sent from the panel to the NAS on UDP port 1700 (MikroTik default) or 3799 (RFC default). It carries the authentication attributes that identify the session (User-Name, Acct-Session-Id) plus the new attributes the panel wants applied. The NAS replies CoA-ACK or CoA-NAK.
Two flavors:
| Packet | Effect on the session | Used for |
|---|---|---|
| CoA-Request | Session continues, attributes are reapplied | Speed plan change, FUP tier flip, time-of-day bandwidth rule, free-hours toggle |
| Disconnect-Request | Session terminates immediately | Operator clicks “Disconnect”, subscriber expires, MAC binding conflict, account suspended, static-IP-conflict auto-resolution |
Both packets are unauthenticated UDP — the NAS validates them by re-computing the message-authenticator against the shared secret.
How ProxPanel sends CoA
Section titled “How ProxPanel sends CoA”internal/radius/coa.go exposes four methods on COAClient:
| Method | Native / shell-out | When used |
|---|---|---|
UpdateRateLimit(username, sessionID, rateLimit) | Native Go via layeh.com/radius | MikroTik backend — the primary path. |
UpdateFilterID(username, sessionID, filterID) | Native Go via layeh.com/radius | Generic backend (Cisco / Juniper / Huawei) — sends Filter-Id (attr 11) instead. |
UpdateRateLimitViaRadclient(...) | Shells out to radclient -x ... coa | Fallback when native CoA mysteriously fails — FreeRADIUS dictionaries handle vendor VSAs that the native client encoded incorrectly. |
DisconnectUser(username, sessionID) and DisconnectViaRadclient(...) | Both | Same patterns for Disconnect-Request. |
The native path is preferred because it doesn’t fork a process. The radclient path exists because of a class of integration issues where the panel’s CoA was silently NAK’d but the same packet sent by radclient was ACK’d — the dictionary handling difference was the culprit.
For radclient to work, the API container must have freeradius-utils installed. The image bakes it in; if it’s missing on an upgrade, apt-get install -y freeradius-utils inside the container fixes it.
The Session-Id rule (lowercase or it doesn’t work)
Section titled “The Session-Id rule (lowercase or it doesn’t work)”This is the single most common silent failure mode and the one users hit most often:
RouterOS stores Acct-Session-Id as uppercase hex with a 0x prefix. CoA against that session will be silently rejected unless ProxPanel sends the Session-Id as lowercase hex with no 0x prefix.
ProxPanel normalizes automatically — coa.go strips 0x / 0X and lowercases — but it logs both forms so you can see what was sent. From the radius logs:
CoA: Sending rate-limit change to <bng-private>:1700 for user=ali@example, session_orig=0xA3F4D1B2 session_sent=a3f4d1b2, rate=2000k/4000ksession_orig is the value MikroTik sent in the Accounting-Start packet. session_sent is what ProxPanel actually put on the wire. If you ever debug a CoA that “did nothing,” look at these two log lines first — if session_sent is not the lowercase-no-prefix form of session_orig, file a bug.
Wire format
Section titled “Wire format”A CoA-Request for a MikroTik speed change carries these attributes:
| Attribute | Type | Source / Value |
|---|---|---|
User-Name | 1 (RFC-2865) | Subscriber username, e.g. ali@example |
Acct-Session-Id | 44 (RFC-2866) | Lowercase hex, no 0x |
Acct-Status-Type | 40 | 48 (MikroTik-required filler) |
Acct-Delay-Time | 41 | 48 (MikroTik-required filler) |
Acct-Input-Octets | 42 | 48 (MikroTik-required filler) |
Vendor-Specific | 26 | MikroTik vendor ID 14988, sub-type 8 (Rate-Limit), value "2000k/4000k" |
The three “filler” Acct attributes are not used by the panel but the MikroTik CoA handler requires their presence — without them the request is silently dropped. The radius VSA encoding (Vendor-ID 4 bytes, Vendor-Type 1, Vendor-Length 1, Value N) is done by hand in coa.go because the upstream library’s attribute-add helper doesn’t expose the right shape.
For generic mode, replace the vendor-specific with a single attribute:
| Attribute | Type | Value |
|---|---|---|
Filter-Id | 11 (RFC-2865) | Policy name, e.g. POLICY_FUP_TIER_1 |
The remaining auth attributes (User-Name, Acct-Session-Id) are identical.
The fallback chain
Section titled “The fallback chain”When ProxPanel wants to change a subscriber’s speed, it doesn’t just try one thing. The handlers (internal/services/quota_sync.go, bandwidth_rule_service.go) attempt three paths in order:
- Native CoA via
COAClient.UpdateRateLimit. Fast, no fork, ACK in ~50 ms typical. - MikroTik API — call
/queue/simple/seton the dynamic queue named after the subscriber’s PPPoE username. This works even if CoA silently NAK’d because RouterOS got a fresh attribute set via API. - radclient via
UpdateRateLimitViaRadclient. Last resort. Useful when MikroTik’s API user is locked or the VSA encoding looked off.
Every attempt logs success / failure. If all three fail, the speed change is recorded as pending and retried on the next QuotaSync tick.
For Disconnect, the fallback is shorter (no API equivalent that’s as clean):
- Native Disconnect via
COAClient.DisconnectUser. - radclient disconnect via
DisconnectViaRadclient.
If both fail, the subscriber stays online — the operator can retry from the UI.
Silent NAKs and why they happen
Section titled “Silent NAKs and why they happen”A CoA-Request can fail in three distinct ways:
| Mode | What happens | How you’ll see it |
|---|---|---|
| Connection error | Network drop, NAS unreachable | failed to send CoA: ... in radius logs |
| CoA-NAK | NAS rejected the change explicitly | CoA NAK received - NAS rejected the request |
| Silent drop | NAS received the packet but pretended it didn’t exist | No reply at all — read timeout |
Silent drops are the worst. They happen when:
- The Session-Id was uppercase or had a
0xprefix (the v1.0.387 era; ProxPanel now normalizes). - The Acct filler attributes were missing.
- The MikroTik’s
/radius incomingis set toaccept=no. - The shared secret on the CoA-port-source differs from the auth-port secret (rare; usually they’re the same).
- The CoA port reachability is broken at L3 (try
nc -uvz <nas-ip> 1700from the panel).
ProxPanel mitigates silent drops by setting a 5-second read deadline on the CoA UDP socket. If no reply lands in 5 s, the call returns “failed to read CoA response” and the fallback chain advances.
Common workflows
Section titled “Common workflows”Manual disconnect from the UI
Section titled “Manual disconnect from the UI”- Operator clicks Disconnect on a subscriber row (or selects multiple and uses the bulk action).
- The handler looks up the NAS from the subscriber’s active session, constructs a
COAClientwith that NAS’s IP / CoA port / secret. DisconnectUser(username, sessionID)fires — Session-Id is normalized first.- If the NAS sends Disconnect-ACK, the radacct row is closed with
acctterminatecause = "Admin-Reset"andsubscribers.is_online = false. - If the NAS sends Disconnect-NAK or the call times out, the operator gets a toast and the session stays open. They can retry or use the MikroTik API fallback (a separate button on the Sessions page).
Mid-session speed plan upgrade
Section titled “Mid-session speed plan upgrade”- Operator changes a subscriber’s
service_idin the panel. - The
Updatehandler computes the newMikrotik-Rate-Limitvalue from the new service. - Native CoA fires immediately (
UpdateRateLimit). - If ACK, the radreply table is updated so the next reconnect uses the new value. Done.
- If NAK or silent drop, fallback to MikroTik API (
/queue/simple/set). If that fails, fallback toradclient. - If all three fail, the change is recorded as pending and the next QuotaSync tick retries.
Time-of-day bandwidth rule kicking in
Section titled “Time-of-day bandwidth rule kicking in”BandwidthRuleServiceticks every 30 s, checks the current time against each rule’s window.- For each subscriber covered by an active rule, computes the new effective rate (base × (100 + boost) / 100 — see Speed Format).
- Sends CoA per session. At 5K online subscribers, this is ~5K CoA packets in a single tick. The native path handles this fine; CPU on the panel server stays under 5%.
- At the end of the window, the inverse CoA restores base speed.
Troubleshooting recipes
Section titled “Troubleshooting recipes””CoA seems to do nothing”
Section titled “”CoA seems to do nothing””- Check the radius logs for the
session_orig=/session_sent=line. Confirmsession_sentis lowercase hex without0x. - On the NAS, confirm the CoA port matches what’s in the NAS row (1700 vs 3799).
- From the panel host:
nc -uvz <nas-ip> 1700(or 3799) — must succeed. - On MikroTik,
/radius incoming printmust showaccept=yes. On Cisco,aaa server radius dynamic-authormust be defined. - Enable MikroTik radius debug logging and watch
/log print followwhile triggering the CoA from the panel.
”Disconnect works but speed-change doesn’t”
Section titled “”Disconnect works but speed-change doesn’t””This is almost always a VSA encoding mismatch. Native CoA constructs the MikroTik VSA by hand; if your MikroTik is on a very old RouterOS (< 6.40) the parser is stricter.
- Edit the subscriber to trigger the speed change. Watch radius logs for
CoA NAK received. - If you see NAK with no further detail, fall back to
radclient: temporarily flip the speed-change path toUpdateRateLimitViaRadclient(a hidden setting under Settings → RADIUS → CoA mode). - Confirm it works via radclient. If yes, you’ve identified a VSA dictionary mismatch — upgrade RouterOS to 6.45+ and switch back to native.
Related pages
Section titled “Related pages”- RADIUS Server Setup — the panel side ports including CoA outbound.
- MikroTik Integration —
/radius incomingconfiguration. - Generic RADIUS — Filter-Id-based CoA.
- Speed Format (kb vs M) — what goes in the
Mikrotik-Rate-Limitvalue. - Stale Session Cleanup — what happens to sessions when CoA Disconnect goes missing.