Static IP Assignment
A static IP in ProxPanel is a Framed-IP-Address row in the radreply table for one specific subscriber. When that subscriber authenticates, the RADIUS server pulls the row, sets it in the Access-Accept, and the BNG hands them that exact IP — every time, on every reconnect.
This page covers how to assign a static IP, the validation that prevents bad data, the duplicate-IP check at write time, and the runtime conflict auto-resolution that picks a new IP after three failed retries.
When to use a static IP
Section titled “When to use a static IP”Pool allocation is the default and the right answer for ~95% of subscribers. Use a static IP when:
- Port-forwarding — the customer hosts a service on their LAN.
- Site-to-site / VPN — the firewall on the other side filters by source IP.
- VoIP regulatory compliance — some jurisdictions require a stable identifier per line.
- Public IP rentals — see Public IPs (the user-rented public IP feature is built on top of this same
radreplymechanism).
If none of the above apply, leave the subscriber on pool allocation. A static IP that doesn’t need to be static is a future support ticket — the IP gets stuck if you delete the subscriber, the customer moves, or the pool is renumbered.
How to assign a static IP
Section titled “How to assign a static IP”- Open the subscriber in Subscribers → Edit.
- Tick Static IP. A text field appears.
- Enter an IPv4 address (e.g.
<subscriber-ip>). Validation runs on save:- Must be a valid IPv4 dotted-quad — IPv6 is rejected (added in v1.0.551).
- Must not already exist in
radreplyfor a different username — see duplicate-IP enforcement. - Should fall within an IP-pool subnet for the subscriber’s service (a warning, not a hard reject — out-of-pool IPs work as long as the BNG routes them).
- Save. The handler:
- Upserts
radreply (username, attribute='Framed-IP-Address', op=':=', value='<subscriber-ip>'). - Sets
subscribers.static_ipfor display. - Marks the IP in
ip_pool_assignmentsasin_useso pool allocation skips it.
- Upserts
- If the subscriber is currently online, the panel sends a CoA Disconnect (see CoA & Disconnect). The next reconnect picks up the new static IP. Without the disconnect, the live session keeps its old pool-allocated IP until next reconnect anyway.
Bulk assignment
Section titled “Bulk assignment”For 100+ subscribers, use Subscribers → Bulk Actions → Set Static IP with a CSV of username,ip. The bulk handler:
- Validates each row.
- Skips rows where the IP is already in
radreplyfor a different username (logged withWARN: Duplicate IP <ip> already in use by <other>). - Inserts the rest in a single transaction.
The duplicate-skip behavior was added in v1.0.226 after a real-world incident where the bulk action created 5 duplicates that took hours to track down.
Duplicate-IP enforcement
Section titled “Duplicate-IP enforcement”The system blocks an IP from being assigned to a second subscriber at four different code paths, defense-in-depth:
| Path | Where | Behavior |
|---|---|---|
| 1. Bulk set-static-IP | internal/handlers/subscriber.go | Skips and logs. Added v1.0.226. |
2. ippool.AllocateIP (pool path) | internal/ippool/pool.go | NOT EXISTS query against radreply excludes any IP already statically assigned. Added v1.0.179. |
3. RADIUS server findAvailableIP | internal/radius/server.go | Checks radreply alongside subscribers.ip_address and subscribers.static_ip. Added v1.0.227. |
| 4. Update handler (single subscriber edit) | internal/handlers/subscriber.go | Pre-flight query rejects the save with a 400 if the IP is taken. |
The first three were each added after a duplicate was observed in production — they patch every code path that could ever create one.
Runtime conflict auto-resolution
Section titled “Runtime conflict auto-resolution”Even with all the write-time checks, a runtime conflict can still happen — usually when a MikroTik’s PPP profile is misconfigured and the router assigns from its own pool, ignoring the Framed-IP-Address the panel sent. Two subscribers end up live with the same IP.
The RADIUS server detects this at auth time. When a subscriber tries to authenticate and the IP they would receive is already held by another online subscriber, the server:
- Sends a CoA Disconnect to the conflicting session (the other user with that IP).
- Lets the new auth proceed.
- Increments a counter in
staticIPConflicts(async.Map) keyed byusername:conflicting_ip.
The kick is single-shot. But what if the same IP keeps conflicting because the BNG keeps reassigning it? An infinite kick loop is the failure mode.
After 3 kicks, ProxPanel breaks the cycle by calling findAvailableIP(conflictIP). This:
- Computes the
/24subnet from the conflicting IP (e.g.10.180.96). - Lists every IP currently in use by online subscribers in that subnet.
- Lists every static IP in that subnet from
subscribers.static_ip. - Lists every IP already in
radreplyFramed-IP-Address for that subnet. - Iterates
<subscriber-ip>through<subscriber-ip>and returns the first IP that’s in none of the three lists.
The new IP is written to radreply for the affected subscriber, and the auth proceeds. The next time they connect, they get the fresh IP without conflict.
If findAvailableIP returns an empty string (the entire /24 is allocated), the conflict counter is kept (not deleted) so the next auth attempt retries instead of silently allowing a duplicate. This was a v1.0.388 fix — previously the counter was cleared on full-pool, opening a small race.
How the auto-resolver picks IPs
Section titled “How the auto-resolver picks IPs”findAvailableIP starts the search at .10 (avoiding .1 gateway, .2–.9 traditionally-reserved) and stops at .249. The first free IP wins.
| Subnet position | Reason |
|---|---|
.1 | Conventional gateway address. |
.2–.9 | Reserved for infrastructure (NAS, network management, BGP peers). |
.10–.249 | Allocation range. |
.250–.254 | Reserved for late-arrival use (broadcast offset, special-purpose). |
.255 | Broadcast. |
This is a heuristic, not a configurable. If your network layout uses different conventions, the function still works — it’ll just skip the first 8 addresses regardless of whether you actually use them.
IPv4 validation (v1.0.551)
Section titled “IPv4 validation (v1.0.551)”Before v1.0.551, the validation regex on the static IP field allowed IPv6 strings to slip through. The RADIUS server then tried to put an IPv6 address into a Framed-IP-Address (RFC-2865 attribute 8), which expects 4 bytes. The auth silently failed because the attribute encoder dropped malformed values.
The fix:
ip := net.ParseIP(input).To4()if ip == nil { return errors.New("must be a valid IPv4 address")}For IPv6 static assignment, use the Framed-IPv6-Address attribute (RFC-3162 attribute 168). ProxPanel supports IPv6 assignment but it’s a separate field on the subscriber model — see IPv6 rollout.
Conflict counter reset
Section titled “Conflict counter reset”The staticIPConflicts sync.Map is in-memory. It resets when the RADIUS container restarts. This is intentional — after a restart, network state is unknown and old conflict counts are meaningless. The first auth after restart gets a fresh kick budget.
If you suspect a runaway conflict cycle and want to clear state immediately, docker restart proxpanel-radius wipes the map.
Troubleshooting
Section titled “Troubleshooting””I set a static IP but the subscriber gets a different one”
Section titled “”I set a static IP but the subscriber gets a different one””Almost always a MikroTik PPP profile issue. See MikroTik Integration → PPP profile — remote-address must be none. If it’s set to a pool name, the static IP is ignored.
”The subscriber keeps getting kicked”
Section titled “”The subscriber keeps getting kicked””They’re in the auto-resolution loop. Watch the RADIUS log:
docker logs proxpanel-radius 2>&1 | grep -i conflictIf you see Conflict for username=<x>: IP <y> in use, kicking session, look at who else has that IP. If the same culprit keeps grabbing it, fix the culprit’s configuration (likely a static IP set on their CPE that overrides what the router assigns).
”Auto-resolution picked an out-of-pool IP”
Section titled “”Auto-resolution picked an out-of-pool IP””The function scans the /24 subnet of the conflicting IP, not the subscriber’s service pool. If your network has subnets that span multiple pools, this picks the first free /24 slot — which might not match the service plan. The fix is to manually set the static IP to a desired address; the auto-resolver only runs when nothing was set explicitly.
Related pages
Section titled “Related pages”- IP Pool Management — pool allocation, the alternative to static IPs.
- MikroTik Integration — the PPP profile setting that makes static IPs honored.
- CoA & Disconnect — how the conflict resolver kicks the other session.
- RADIUS Server Setup — where Framed-IP-Address sits in the reply.
- Public IPs — public-IP rentals are built on this same mechanism.