Parent: #4193
Problem
When a host connects, the CLI looks for an existing user account by client_ip and user_type and ignores the owner entirely:
// crates/doublezero-daemon-cli/src/connect.rs:1281-1284
// Find user by both client_ip AND user_type to support multiple tunnel types per IP
let matched_user = users
.iter()
.find(|(_, u)| u.client_ip == *client_ip && u.user_type == user_type);
If a previous tenant of that IP left a user account onchain, the CLI prints An account already exists with Pubkey: X and adopts it. The daemon then does the same thing independently, filtering onchain users by client IP alone:
// client/doublezerod/internal/manager/manager.go:507-512
for _, u := range data.Users {
userIP := net.IP(u.ClientIp[:])
if !userIP.Equal(n.clientIP) {
continue
}
The controller has already programmed the device tunnel with UnderlayDstIP = <client_ip> (controlplane/controller/internal/controller/server.go:616), so packets reach the new host and the session comes up. The new operator is connected on someone else's account: someone else's dz_ip, device seat, access pass, multicast allowlists, and billing attribution. Nothing onchain records that the machine changed hands.
doublezero disconnect then refuses, because it skips users whose owner is not the local payer (crates/doublezero-daemon-cli/src/disconnect.rs:147-165), and reports the account as "managed by an external service". That message is the operator-visible symptom of this bug.
Why this blocks the rest of the work
Any reclaim rule that asks "is the incumbent still alive?" reads bgp_status on the user account. While the CLI and daemon keep adopting stale accounts, the new tenant is the one bringing that session up, so the stale account reports Up and looks healthy. This fix has to land before a liveness gate means anything.
Proposed change
- In
find_or_create_user (and the equivalent multicast paths), only adopt an existing user when u.owner == ledger.get_payer(). On a mismatch, fail with a diagnosis the operator can act on, naming the IP, the owning keypair, and the fact that this is most likely a previous tenant of the address.
- Apply the same owner filter in the daemon reconciler so a stale foreign account cannot provision a tunnel on a host that never asked for one.
- Reword the
disconnect message for the owner-mismatch case. "Managed by an external service" is accurate for the shred-oracle path but misleading here.
Testing verification
- Connect with a user account present for the same
(client_ip, user_type) under a different owner; the CLI must refuse rather than adopt.
- Daemon reconciler test: a stale foreign user at the local client IP produces no tunnel.
- Existing shred-oracle-owned-user path still reports the
doublezero-solana shreds withdraw guidance.
Parent: #4193
Problem
When a host connects, the CLI looks for an existing user account by
client_ipanduser_typeand ignores the owner entirely:If a previous tenant of that IP left a user account onchain, the CLI prints
An account already exists with Pubkey: Xand adopts it. The daemon then does the same thing independently, filtering onchain users by client IP alone:The controller has already programmed the device tunnel with
UnderlayDstIP = <client_ip>(controlplane/controller/internal/controller/server.go:616), so packets reach the new host and the session comes up. The new operator is connected on someone else's account: someone else'sdz_ip, device seat, access pass, multicast allowlists, and billing attribution. Nothing onchain records that the machine changed hands.doublezero disconnectthen refuses, because it skips users whose owner is not the local payer (crates/doublezero-daemon-cli/src/disconnect.rs:147-165), and reports the account as "managed by an external service". That message is the operator-visible symptom of this bug.Why this blocks the rest of the work
Any reclaim rule that asks "is the incumbent still alive?" reads
bgp_statuson the user account. While the CLI and daemon keep adopting stale accounts, the new tenant is the one bringing that session up, so the stale account reports Up and looks healthy. This fix has to land before a liveness gate means anything.Proposed change
find_or_create_user(and the equivalent multicast paths), only adopt an existing user whenu.owner == ledger.get_payer(). On a mismatch, fail with a diagnosis the operator can act on, naming the IP, the owning keypair, and the fact that this is most likely a previous tenant of the address.disconnectmessage for the owner-mismatch case. "Managed by an external service" is accurate for the shred-oracle path but misleading here.Testing verification
(client_ip, user_type)under a different owner; the CLI must refuse rather than adopt.doublezero-solana shreds withdrawguidance.