mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
SaveProxy upserts on the proxy ID, so on a reconnect the row Connect just wrote is the claim the account has held since its first connect, and the session guard on DeleteProxy matches because the upsert wrote the new session. Withdrawing that row whenever the post-write re-read errored surrendered an established claim on a transient store error — a window in which any other account could take the address — where the pre-existing code left the row untouched. An inconclusive re-read still refuses the connect, but marks the session disconnected instead of deleting the row; only a conclusive answer that the address is claimed withdraws it. The write-then-re-read argument moves to the doc of the exported ErrClusterAddressUnavailable, where the API needs it, and both helpers point there instead of carrying it twice. Store-backed tests drive the re-read through the real queries — a reconnect keeps its row, the account's own pin is not a competing claim, another account's is — since the whole path now depends on the store excluding the account's own claims. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
99 lines
4.0 KiB
Go
99 lines
4.0 KiB
Go
package proxy
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
StatusConnected = "connected"
|
|
StatusDisconnected = "disconnected"
|
|
)
|
|
|
|
// ErrClusterAddressUnavailable is returned by Manager.Connect when the cluster
|
|
// address turns out to be claimed by someone else once the proxy's own row is
|
|
// written: a conflicting proxy row, or another account's agent network gateway
|
|
// pinned to the address. The row has been withdrawn by then, and the caller
|
|
// reports the address as taken exactly as if the pre-write check had caught it.
|
|
//
|
|
// Both kinds of claim are made the same way, write then re-read then withdraw,
|
|
// and the re-read is the whole mechanism. Each side's availability check and
|
|
// its write are separate autocommit statements, so two concurrent claimants
|
|
// can each pass their check with neither row committed yet. Because both write
|
|
// before they re-read, of two concurrent claims at least one re-reads after
|
|
// the other has committed and backs off; each statement sees every commit
|
|
// before it on sqlite, postgres and mysql alike. 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. The gateway side of the same protocol is
|
|
// agentnetwork's confirmGatewayClusterOwnership.
|
|
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
|
|
}
|