Skip to content

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.

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:

PacketEffect on the sessionUsed for
CoA-RequestSession continues, attributes are reappliedSpeed plan change, FUP tier flip, time-of-day bandwidth rule, free-hours toggle
Disconnect-RequestSession terminates immediatelyOperator 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.

internal/radius/coa.go exposes four methods on COAClient:

MethodNative / shell-outWhen used
UpdateRateLimit(username, sessionID, rateLimit)Native Go via layeh.com/radiusMikroTik backend — the primary path.
UpdateFilterID(username, sessionID, filterID)Native Go via layeh.com/radiusGeneric backend (Cisco / Juniper / Huawei) — sends Filter-Id (attr 11) instead.
UpdateRateLimitViaRadclient(...)Shells out to radclient -x ... coaFallback when native CoA mysteriously fails — FreeRADIUS dictionaries handle vendor VSAs that the native client encoded incorrectly.
DisconnectUser(username, sessionID) and DisconnectViaRadclient(...)BothSame 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/4000k

session_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.

A CoA-Request for a MikroTik speed change carries these attributes:

AttributeTypeSource / Value
User-Name1 (RFC-2865)Subscriber username, e.g. ali@example
Acct-Session-Id44 (RFC-2866)Lowercase hex, no 0x
Acct-Status-Type4048 (MikroTik-required filler)
Acct-Delay-Time4148 (MikroTik-required filler)
Acct-Input-Octets4248 (MikroTik-required filler)
Vendor-Specific26MikroTik 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:

AttributeTypeValue
Filter-Id11 (RFC-2865)Policy name, e.g. POLICY_FUP_TIER_1

The remaining auth attributes (User-Name, Acct-Session-Id) are identical.

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:

  1. Native CoA via COAClient.UpdateRateLimit. Fast, no fork, ACK in ~50 ms typical.
  2. MikroTik API — call /queue/simple/set on 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.
  3. 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):

  1. Native Disconnect via COAClient.DisconnectUser.
  2. radclient disconnect via DisconnectViaRadclient.

If both fail, the subscriber stays online — the operator can retry from the UI.

A CoA-Request can fail in three distinct ways:

ModeWhat happensHow you’ll see it
Connection errorNetwork drop, NAS unreachablefailed to send CoA: ... in radius logs
CoA-NAKNAS rejected the change explicitlyCoA NAK received - NAS rejected the request
Silent dropNAS received the packet but pretended it didn’t existNo reply at all — read timeout

Silent drops are the worst. They happen when:

  • The Session-Id was uppercase or had a 0x prefix (the v1.0.387 era; ProxPanel now normalizes).
  • The Acct filler attributes were missing.
  • The MikroTik’s /radius incoming is set to accept=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> 1700 from 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.

  1. Operator clicks Disconnect on a subscriber row (or selects multiple and uses the bulk action).
  2. The handler looks up the NAS from the subscriber’s active session, constructs a COAClient with that NAS’s IP / CoA port / secret.
  3. DisconnectUser(username, sessionID) fires — Session-Id is normalized first.
  4. If the NAS sends Disconnect-ACK, the radacct row is closed with acctterminatecause = "Admin-Reset" and subscribers.is_online = false.
  5. 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).
  1. Operator changes a subscriber’s service_id in the panel.
  2. The Update handler computes the new Mikrotik-Rate-Limit value from the new service.
  3. Native CoA fires immediately (UpdateRateLimit).
  4. If ACK, the radreply table is updated so the next reconnect uses the new value. Done.
  5. If NAK or silent drop, fallback to MikroTik API (/queue/simple/set). If that fails, fallback to radclient.
  6. If all three fail, the change is recorded as pending and the next QuotaSync tick retries.
  1. BandwidthRuleService ticks every 30 s, checks the current time against each rule’s window.
  2. For each subscriber covered by an active rule, computes the new effective rate (base × (100 + boost) / 100 — see Speed Format).
  3. 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%.
  4. At the end of the window, the inverse CoA restores base speed.
  1. Check the radius logs for the session_orig= / session_sent= line. Confirm session_sent is lowercase hex without 0x.
  2. On the NAS, confirm the CoA port matches what’s in the NAS row (1700 vs 3799).
  3. From the panel host: nc -uvz <nas-ip> 1700 (or 3799) — must succeed.
  4. On MikroTik, /radius incoming print must show accept=yes. On Cisco, aaa server radius dynamic-author must be defined.
  5. Enable MikroTik radius debug logging and watch /log print follow while 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.

  1. Edit the subscriber to trigger the speed change. Watch radius logs for CoA NAK received.
  2. If you see NAK with no further detail, fall back to radclient: temporarily flip the speed-change path to UpdateRateLimitViaRadclient (a hidden setting under Settings → RADIUS → CoA mode).
  3. Confirm it works via radclient. If yes, you’ve identified a VSA dictionary mismatch — upgrade RouterOS to 6.45+ and switch back to native.