Files
netbird/management/internals/modules/reverseproxy/proxy/proxy.go
T
mlsmayconandClaude Fable 5.1 68da6bf3aa [management] Withdraw a cluster address claim that is lost after the write
A cluster address is claimed two ways: an account-scoped proxy row, and an
agent network gateway pin on the address. Each side checked the other
before writing — IsClusterAddressAvailable before SaveProxy,
HasForeignAccountProxyAtHost before the settings insert — but check and
write are separate autocommit statements, so two concurrent claimants could
each pass their check and both commit, leaving a pin no proxy will ever
serve next to the proxy row that displaces it.

Both sides now re-read after they write. Manager.Connect re-asks
availability once the proxy row is committed and, if the address is no
longer free or the answer is inconclusive, deletes its own row and returns
ErrClusterAddressUnavailable, which the connect path reports as
AlreadyExists exactly as the pre-write check would have. bootstrapLabeled
re-asks ownership once the settings row is committed and withdraws the pin
on the same terms. Because both write before they re-read, of two
concurrent claimants at least one re-reads after the other has committed
and backs off — on sqlite, postgres and mysql alike, since each statement
sees every commit before it. Both may back off, which costs a retry;
neither keeps a claim the other holds.

No lock spans the proxies and settings tables portably, and a claims table
would be more machinery than the property needs, so the re-read is the
whole mechanism. DeleteProxy is session-guarded like DisconnectProxy, so a
stale session withdrawing itself cannot take out a newer session's row.

Reported by CodeRabbit on #7402 (CWE-362).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
2026-09-12 12:53:15 +00:00

88 lines
3.3 KiB
Go

package proxy
import (
"errors"
"time"
)
const (
StatusConnected = "connected"
StatusDisconnected = "disconnected"
)
// ErrClusterAddressUnavailable is returned by Manager.Connect when the
// cluster address turned out to be claimed by someone else once the proxy's
// own row was written — a conflicting proxy row or another account's agent
// network gateway pin that landed between the availability check and the
// write. The proxy's row has been withdrawn by then; the caller reports the
// address as taken, exactly as if the pre-write check had caught it.
var ErrClusterAddressUnavailable = errors.New("cluster address is not available")
// Capabilities describes what a proxy can handle, as reported via gRPC.
// Nil fields mean the proxy never reported this capability.
type Capabilities struct {
// SupportsCustomPorts indicates whether this proxy can bind arbitrary
// ports for TCP/UDP services. TLS uses SNI routing and is not gated.
SupportsCustomPorts *bool
// RequireSubdomain indicates whether a subdomain label is required in
// front of the cluster domain.
RequireSubdomain *bool
// SupportsCrowdsec indicates whether this proxy has CrowdSec configured.
SupportsCrowdsec *bool
// Private indicates whether this proxy supports inbound access via Wireguard
// tunnel and netbird-only authentication policies
Private *bool
}
// Proxy represents a reverse proxy instance
type Proxy struct {
ID string `gorm:"primaryKey;type:varchar(255)"`
SessionID string `gorm:"type:varchar(36)"`
ClusterAddress string `gorm:"type:varchar(255);not null;index:idx_proxy_cluster_status"`
IPAddress string `gorm:"type:varchar(45)"`
AccountID *string `gorm:"type:varchar(255);index:idx_proxy_account_id"`
LastSeen time.Time `gorm:"not null;index:idx_proxy_last_seen"`
ConnectedAt *time.Time
DisconnectedAt *time.Time
Status string `gorm:"type:varchar(20);not null;index:idx_proxy_cluster_status"`
Capabilities Capabilities `gorm:"embedded"`
CreatedAt time.Time
UpdatedAt time.Time
}
func (Proxy) TableName() string {
return "proxies"
}
// ClusterType is the source of a proxy cluster.
type ClusterType string
const (
// ClusterTypeAccount is a cluster operated by the account itself (BYOP) —
// at least one proxy row in the cluster carries a non-NULL account_id.
ClusterTypeAccount ClusterType = "account"
// ClusterTypeShared is a cluster operated by NetBird and shared across
// accounts — all proxy rows in the cluster have account_id IS NULL.
ClusterTypeShared ClusterType = "shared"
)
// Cluster represents a group of proxy nodes serving the same address.
//
// Online and ConnectedProxies derive from the same 2-min active window
// the rest of the module uses, but Cluster rows are not gated on it —
// the cluster listing surfaces offline clusters too so operators can
// see and clean them up. The 1-hour heartbeat reaper still bounds the
// table eventually.
type Cluster struct {
ID string
Address string
Type ClusterType
Online bool
ConnectedProxies int
// *bool: nil = no proxy reported the capability; the dashboard renders that as unknown.
SupportsCustomPorts *bool
RequireSubdomain *bool
SupportsCrowdSec *bool
Private *bool
}