Skip to content

client: refuse to adopt a user account owned by a different keypair #4188

Description

@elitegreg

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions