Skip to content

Duplicate IPs

Two subscribers ended up with the same IP. They alternate connectivity, one drops every few minutes when the other reconnects, support tickets multiply. This used to be a real pain on early ProxPanel installs — four code paths could assign the same IP without checking each other. The current four-layer defense fixes it for new allocations; this page covers detection, the four causes, and how to clean up the historical mess.

  • Two PPPoE users disconnect each other in a loop
  • /ip arp print on the MikroTik shows the same IP claimed by different MACs
  • Customer A says “internet works for 30 seconds, then dies for 30 seconds”
  • Subscriber detail page shows the same ip_address on two different users
  • radacct shows overlapping sessions on the same framedipaddress
  1. Look in radreply first — that’s the static-assignment table.

    Terminal window
    docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
    "SELECT value AS ip, COUNT(*) AS users, STRING_AGG(username, ', ') AS usernames \
    FROM radreply WHERE attribute='Framed-IP-Address' \
    GROUP BY value HAVING COUNT(*) > 1;"

    Each row is a duplicate.

  2. Then look in subscribers — who is currently online on a shared IP.

    Terminal window
    docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
    "SELECT ip_address, COUNT(*) AS count, STRING_AGG(username, ', ') AS users \
    FROM subscribers WHERE is_online=true AND ip_address IS NOT NULL \
    GROUP BY ip_address HAVING COUNT(*) > 1;"

    Both queries should return zero rows on a healthy install.

  3. Check ip_pool_assignments — this is ProxPanel’s allocation ledger.

    Terminal window
    docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
    "SELECT ip_address, status, username FROM ip_pool_assignments \
    WHERE ip_address = '<subscriber-ip>';"

    Expect status='in_use' and a single username for any IP that’s allocated.

  4. For each duplicate, decide which subscriber keeps the IP. Usually whichever user dialled it longer (highest acctstarttime in radacct) is the owner. The other gets reassigned.

There are four places in the code that can assign an IP. Before v1.0.227 they did not all check each other; now they all do. Knowing which one created the duplicate tells you which logfile to look at.

Code pathFileFunctionWhat it does
AllocateIPinternal/ippool/pool.goAllocateIP()Picks an IP from a pool when a session needs one (PPPoE Accounting-Start). Checks both ip_pool_assignments and radreply.
set_static_ip bulk actioninternal/handlers/subscriber.goBulkActionAdmin manually assigns a static IP. Refuses duplicates as of v1.0.226.
findAvailableIPinternal/radius/server.gofindAvailableIP()Conflict resolver — when a static IP collision is detected during auth, picks a substitute from the same /24. Now checks radreply as of v1.0.227.
Service changeinternal/handlers/subscriber.goChangeService, UpdateDeletes the old Framed-IP-Address so the user gets a new pool IP on reconnect. Doesn’t allocate directly.

If a duplicate appears after v1.0.227, one of these checks failed — that’s worth a bug report.

Cause 1 — Historical duplicates from before v1.0.226

Section titled “Cause 1 — Historical duplicates from before v1.0.226”

Installs that ran for months on early versions accumulate duplicates that were never cleaned up. The current allocation logic refuses to create new ones but doesn’t go back and audit old data.

Diagnostic: the SELECT queries in the Diagnostic flow above.

Fix — assign a fresh IP to one of the duplicate users:

  1. Back up radreply:

    Terminal window
    docker exec proxpanel-db pg_dump -U proxpanel -d proxpanel -t radreply > /tmp/radreply-backup.sql
  2. For each duplicate, pick the loser — usually the user who is not currently online:

    Terminal window
    docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
    "SELECT s.username, s.is_online, r.value AS ip \
    FROM radreply r JOIN subscribers s ON s.username = r.username \
    WHERE r.attribute='Framed-IP-Address' AND r.value = '<subscriber-ip>';"
  3. Delete the loser’s static IP entry. On next reconnect, AllocateIP will pick a free one from the pool:

    DELETE FROM radreply
    WHERE username = 'LOSER_USERNAME' AND attribute = 'Framed-IP-Address';
  4. Disconnect the loser to force a reconnect with the new IP:

    • Subscribers list → find user → action menu → Disconnect.
    • Or send a CoA / Disconnect from the panel; the user reconnects within seconds.
  5. Confirm:

    Terminal window
    docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
    "SELECT username, value FROM radreply WHERE attribute='Framed-IP-Address' AND value='<subscriber-ip>';"

    One row, one username.

Cause 2 — ip_pool_assignments has the wrong pool tags

Section titled “Cause 2 — ip_pool_assignments has the wrong pool tags”

The pool ledger was imported with bad pool boundaries (e.g. <subscriber-ip> tagged as “3M” when it actually belongs to the “2M” subnet). New allocations from the wrong pool produce IPs that overlap with another pool’s range.

Diagnostic:

Terminal window
# Pull what the MikroTik says are the real pool ranges:
# (on the MikroTik): /ip pool print
# Compare to ip_pool_assignments grouped by pool:
docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
"SELECT pool_name, MIN(ip_address), MAX(ip_address), COUNT(*) \
FROM ip_pool_assignments GROUP BY pool_name ORDER BY pool_name;"

If a pool’s MIN/MAX overlaps another pool, the ledger is wrong.

Fix:

Re-import pools from the MikroTik. On the NAS detail page → Refresh / Re-import pools. The handler wipes ip_pool_assignments and rebuilds from the MikroTik’s /ip pool print ranges. Active sessions get re-marked in_use from radacct.

If you’ve added a NAS after this point, the handler auto-imports on NAS creation (since v1.0.164).

Cause 3 — Manual set_static_ip collision (pre-v1.0.226)

Section titled “Cause 3 — Manual set_static_ip collision (pre-v1.0.226)”

An admin used the bulk action to assign an IP that another user already had. v1.0.226 added a pre-check that refuses the assignment with a UI error. Older versions silently overwrote.

Diagnostic:

Terminal window
docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
"SELECT user_name, action, description, created_at \
FROM audit_logs \
WHERE description ILIKE '%static%ip%' OR description ILIKE '%framed-ip%' \
ORDER BY created_at DESC LIMIT 20;"

Fix:

Upgrade past v1.0.226 (you almost certainly already have — current versions are 500+). Then clean up using the Cause 1 procedure.

Cause 4 — Stale radacct sessions claiming IPs forever

Section titled “Cause 4 — Stale radacct sessions claiming IPs forever”

If MikroTik reboots without sending an Accounting-Stop, the session stays open in radacct indefinitely. The IP is still listed as “in use” by that ghost session, so the next allocation skips it. Eventually the pool runs out of IPs and the fallback findAvailableIP reaches for one that’s also tagged “in use” by another ghost. Pre-v1.0.226, this could produce duplicates.

Diagnostic:

Terminal window
docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
"SELECT COUNT(*) AS ghost_sessions \
FROM radacct \
WHERE acctstoptime IS NULL \
AND acctupdatetime < NOW() - INTERVAL '30 minutes';"

If you see a number much bigger than the real online count, you have a ghost session backlog.

Fix:

StaleSessionCleanupService runs every 5 minutes and closes sessions with no interim update in 30+ minutes. It was added in v1.0.226. Confirm it’s running:

Terminal window
docker logs proxpanel-api 2>&1 | grep "StaleSessionCleanup" | tail -10

You should see periodic StaleSessionCleanup: closed N stale sessions, marked M offline lines.

If you need to force a cleanup right now:

UPDATE radacct
SET acctstoptime = NOW(),
acctterminatecause = 'Admin-Reset'
WHERE acctstoptime IS NULL
AND acctupdatetime < NOW() - INTERVAL '30 minutes';

Bulk reassignment — give every subscriber a fresh allocation

Section titled “Bulk reassignment — give every subscriber a fresh allocation”

If duplicates are widespread (you’re staring at 50+ rows from the Diagnostic flow), the fastest cleanup is to wipe Framed-IP-Address for everyone affected and let AllocateIP re-pool them on next reconnect.

-- Backup
\copy radreply TO '/tmp/radreply-bulk-backup.tsv' WITH (FORMAT csv, DELIMITER E'\t', HEADER);
-- Remove all static-IP entries for users involved in any duplicate
DELETE FROM radreply
WHERE attribute = 'Framed-IP-Address'
AND value IN (
SELECT value FROM radreply
WHERE attribute = 'Framed-IP-Address'
GROUP BY value HAVING COUNT(*) > 1
);
-- Then disconnect them (CoA), they reconnect with fresh AllocateIP picks

Use the subscribers list → filter is_online=true → bulk Disconnect. The reconnect storm takes a few minutes; the QuotaSync log shows new IPs being assigned via AllocateIP.

Send:

Terminal window
# Current duplicates
docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
"SELECT value, COUNT(*), STRING_AGG(username, ',') FROM radreply \
WHERE attribute='Framed-IP-Address' GROUP BY value HAVING COUNT(*) > 1;" > /tmp/dups.txt
# Pool ledger health
docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
"SELECT pool_name, status, COUNT(*) FROM ip_pool_assignments \
GROUP BY pool_name, status ORDER BY pool_name, status;" > /tmp/pools.txt
# Stale sessions
docker exec proxpanel-db psql -U proxpanel -d proxpanel -c \
"SELECT COUNT(*) FROM radacct WHERE acctstoptime IS NULL \
AND acctupdatetime < NOW() - INTERVAL '30 minutes';" > /tmp/stale.txt
# Version
curl -s http://localhost:8080/health

Email info@proxrad.com.

  • IP Pools — the ledger and the import-from-NAS flow.
  • PPPoE Auth Issues — when the static-IP conflict resolver kicks the wrong user.
  • NAS / Routers — auto-import of pools on NAS creation.