mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-25 16:19:07 +02:00
782 lines
28 KiB
Go
782 lines
28 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
log "github.com/sirupsen/logrus"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/shared/management/status"
|
|
)
|
|
|
|
func (s *SqlStore) SavePeer(ctx context.Context, accountID string, peer *nbpeer.Peer) error {
|
|
// To maintain data integrity, we create a copy of the peer's to prevent unintended updates to other fields.
|
|
peerCopy := peer.Copy()
|
|
peerCopy.AccountID = accountID
|
|
|
|
err := s.transaction(func(tx *gorm.DB) error {
|
|
// check if peer exists before saving
|
|
var peerID string
|
|
result := tx.Model(&nbpeer.Peer{}).Select("id").Take(&peerID, accountAndIDQueryCondition, accountID, peer.ID)
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)
|
|
}
|
|
return result.Error
|
|
}
|
|
|
|
if peerID == "" {
|
|
return status.Errorf(status.NotFound, peerNotFoundFMT, peer.ID)
|
|
}
|
|
|
|
result = tx.Model(&nbpeer.Peer{}).Where(accountAndIDQueryCondition, accountID, peer.ID).Save(peerCopy)
|
|
if result.Error != nil {
|
|
return status.Errorf(status.Internal, "failed to save peer to store: %v", result.Error)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlStore) SavePeerStatus(ctx context.Context, accountID, peerID string, peerStatus nbpeer.PeerStatus) error {
|
|
var peerCopy nbpeer.Peer
|
|
peerCopy.Status = &peerStatus
|
|
|
|
fieldsToUpdate := []string{
|
|
"peer_status_last_seen", "peer_status_session_started_at",
|
|
"peer_status_connected", "peer_status_login_expired",
|
|
"peer_status_requires_approval",
|
|
}
|
|
result := s.db.Model(&nbpeer.Peer{}).
|
|
Select(fieldsToUpdate).
|
|
Where(accountAndIDQueryCondition, accountID, peerID).
|
|
Updates(&peerCopy)
|
|
if result.Error != nil {
|
|
return status.Errorf(status.Internal, "failed to save peer status to store: %v", result.Error)
|
|
}
|
|
|
|
if result.RowsAffected == 0 {
|
|
return status.Errorf(status.NotFound, peerNotFoundFMT, peerID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarkPeerConnectedIfNewerSession is an atomic optimistic-locked update.
|
|
// The peer is marked connected with the given session token only when
|
|
// the stored SessionStartedAt is strictly smaller than the incoming
|
|
// one — equivalently, when no newer stream has already taken ownership.
|
|
// The sentinel zero (set on peer creation or after a disconnect) counts
|
|
// as the smallest possible token. This is the write half of the
|
|
// fencing protocol described on PeerStatus.SessionStartedAt.
|
|
//
|
|
// The post-write side effects in the caller — geo lookup,
|
|
// schedulePeerLoginExpiration, checkAndSchedulePeerInactivityExpiration,
|
|
// OnPeersUpdated — all run AFTER this method returns and are deliberately
|
|
// outside the database write so they cannot extend the row-lock window.
|
|
//
|
|
// LastSeen is set to the database's clock (CURRENT_TIMESTAMP) at the
|
|
// moment the row is written. The caller never supplies LastSeen because
|
|
// the value would otherwise drift under lock contention — a Go-side
|
|
// time.Now() taken before the write can land minutes later than the
|
|
// actual UPDATE under load, which previously caused real ordering bugs.
|
|
func (s *SqlStore) MarkPeerConnectedIfNewerSession(ctx context.Context, accountID, peerID string, newSessionStartedAt int64) (bool, error) {
|
|
result := s.db.WithContext(ctx).
|
|
Model(&nbpeer.Peer{}).
|
|
Where(accountAndIDQueryCondition, accountID, peerID).
|
|
Where("peer_status_session_started_at < ?", newSessionStartedAt).
|
|
Updates(map[string]any{
|
|
"peer_status_connected": true,
|
|
"peer_status_last_seen": gorm.Expr("CURRENT_TIMESTAMP"),
|
|
"peer_status_session_started_at": newSessionStartedAt,
|
|
"peer_status_login_expired": false,
|
|
})
|
|
if result.Error != nil {
|
|
return false, status.Errorf(status.Internal, "mark peer connected: %v", result.Error)
|
|
}
|
|
return result.RowsAffected > 0, nil
|
|
}
|
|
|
|
// MarkPeerDisconnectedIfSameSession is an atomic optimistic-locked update.
|
|
// The peer is marked disconnected only when the stored SessionStartedAt
|
|
// matches the incoming token — meaning the stream that owns the current
|
|
// session is the one ending. If a newer stream has already replaced the
|
|
// session, the update is skipped. LastSeen is set to CURRENT_TIMESTAMP at
|
|
// write time; see MarkPeerConnectedIfNewerSession for the rationale.
|
|
//
|
|
// A zero sessionStartedAt is rejected at the call site; the underlying
|
|
// WHERE on equality would otherwise match every never-connected peer.
|
|
func (s *SqlStore) MarkPeerDisconnectedIfSameSession(ctx context.Context, accountID, peerID string, sessionStartedAt int64) (bool, error) {
|
|
if sessionStartedAt == 0 {
|
|
return false, nil
|
|
}
|
|
result := s.db.WithContext(ctx).
|
|
Model(&nbpeer.Peer{}).
|
|
Where(accountAndIDQueryCondition, accountID, peerID).
|
|
Where("peer_status_session_started_at = ?", sessionStartedAt).
|
|
Updates(map[string]any{
|
|
"peer_status_connected": false,
|
|
"peer_status_last_seen": gorm.Expr("CURRENT_TIMESTAMP"),
|
|
"peer_status_session_started_at": int64(0),
|
|
})
|
|
if result.Error != nil {
|
|
return false, status.Errorf(status.Internal, "mark peer disconnected: %v", result.Error)
|
|
}
|
|
return result.RowsAffected > 0, nil
|
|
}
|
|
|
|
// ApproveAccountPeers marks all peers that currently require approval in the given account as approved.
|
|
func (s *SqlStore) ApproveAccountPeers(ctx context.Context, accountID string) (int, error) {
|
|
result := s.db.Model(&nbpeer.Peer{}).
|
|
Where("account_id = ? AND peer_status_requires_approval = ?", accountID, true).
|
|
Update("peer_status_requires_approval", false)
|
|
if result.Error != nil {
|
|
return 0, status.Errorf(status.Internal, "failed to approve pending account peers: %v", result.Error)
|
|
}
|
|
|
|
return int(result.RowsAffected), nil
|
|
}
|
|
|
|
// RefreshPeerLastSeen updates only peer_status_last_seen. Every other status
|
|
// column is left untouched: peer_status_connected and
|
|
// peer_status_session_started_at belong to the sync stream that owns the
|
|
// session, and a blind write here would corrupt the fencing
|
|
// MarkPeerConnectedIfNewerSession relies on.
|
|
//
|
|
// LastSeen comes from the database clock for the same reason it does there: a
|
|
// Go-side timestamp is taken before the write and can land after a connect that
|
|
// used CURRENT_TIMESTAMP, dragging the column backwards.
|
|
//
|
|
// staleBefore carries the caller's throttle into the same statement, so
|
|
// concurrent requests for one peer collapse into a single write instead of
|
|
// each racing on its own stale read. The column is nullable — Status is an
|
|
// embedded pointer, so a peer stored without one leaves it NULL — and NULL
|
|
// loses every comparison, hence the explicit branch for a peer never seen.
|
|
func (s *SqlStore) RefreshPeerLastSeen(ctx context.Context, accountID, peerID string, staleBefore time.Time) (bool, error) {
|
|
result := s.db.WithContext(ctx).
|
|
Model(&nbpeer.Peer{}).
|
|
Where(accountAndIDQueryCondition, accountID, peerID).
|
|
Where("(peer_status_last_seen IS NULL OR peer_status_last_seen < ?)", staleBefore).
|
|
Update("peer_status_last_seen", gorm.Expr("CURRENT_TIMESTAMP"))
|
|
if result.Error != nil {
|
|
return false, status.Errorf(status.Internal, "refresh peer last seen: %v", result.Error)
|
|
}
|
|
|
|
return result.RowsAffected > 0, nil
|
|
}
|
|
|
|
func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Peer, error) {
|
|
const query = `SELECT id, account_id, key, ip, name, dns_label, user_id, ssh_key, ssh_enabled, login_expiration_enabled,
|
|
inactivity_expiration_enabled, last_login, created_at, ephemeral, extra_dns_labels, allow_extra_dns_labels, meta_hostname,
|
|
meta_go_os, meta_kernel, meta_core, meta_platform, meta_os, meta_os_version, meta_wt_version, meta_ui_version,
|
|
meta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,
|
|
meta_environment, meta_flags, meta_files, meta_capabilities, peer_status_last_seen, peer_status_session_started_at,
|
|
peer_status_connected, peer_status_login_expired, peer_status_requires_approval, location_connection_ip,
|
|
location_country_code, location_city_name, location_geo_name_id, proxy_meta_embedded, proxy_meta_cluster, ipv6, meta_sync_message_version
|
|
FROM peers WHERE account_id = $1`
|
|
rows, err := s.pool.Query(ctx, query, accountID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
peers, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (nbpeer.Peer, error) {
|
|
var p nbpeer.Peer
|
|
p.Status = &nbpeer.PeerStatus{}
|
|
var (
|
|
lastLogin, createdAt sql.NullTime
|
|
sshEnabled, loginExpirationEnabled, inactivityExpirationEnabled, ephemeral, allowExtraDNSLabels sql.NullBool
|
|
peerStatusLastSeen sql.NullTime
|
|
peerStatusSessionStartedAt sql.NullInt64
|
|
peerStatusConnected, peerStatusLoginExpired, peerStatusRequiresApproval, proxyEmbedded sql.NullBool
|
|
ip, extraDNS, netAddr, env, flags, files, capabilities, connIP, ipv6 []byte
|
|
metaHostname, metaGoOS, metaKernel, metaCore, metaPlatform sql.NullString
|
|
metaOS, metaOSVersion, metaWtVersion, metaUIVersion, metaKernelVersion sql.NullString
|
|
metaSystemSerialNumber, metaSystemProductName, metaSystemManufacturer sql.NullString
|
|
locationCountryCode, locationCityName, proxyCluster sql.NullString
|
|
locationGeoNameID sql.NullInt64
|
|
metaSyncMessageVersion sql.NullInt32
|
|
)
|
|
|
|
err := row.Scan(&p.ID, &p.AccountID, &p.Key, &ip, &p.Name, &p.DNSLabel, &p.UserID, &p.SSHKey, &sshEnabled,
|
|
&loginExpirationEnabled, &inactivityExpirationEnabled, &lastLogin, &createdAt, &ephemeral, &extraDNS,
|
|
&allowExtraDNSLabels, &metaHostname, &metaGoOS, &metaKernel, &metaCore, &metaPlatform,
|
|
&metaOS, &metaOSVersion, &metaWtVersion, &metaUIVersion, &metaKernelVersion, &netAddr,
|
|
&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files, &capabilities,
|
|
&peerStatusLastSeen, &peerStatusSessionStartedAt, &peerStatusConnected, &peerStatusLoginExpired,
|
|
&peerStatusRequiresApproval, &connIP, &locationCountryCode, &locationCityName, &locationGeoNameID,
|
|
&proxyEmbedded, &proxyCluster, &ipv6, &metaSyncMessageVersion)
|
|
|
|
if err == nil {
|
|
if lastLogin.Valid {
|
|
p.LastLogin = &lastLogin.Time
|
|
}
|
|
if createdAt.Valid {
|
|
p.CreatedAt = createdAt.Time
|
|
}
|
|
if sshEnabled.Valid {
|
|
p.SSHEnabled = sshEnabled.Bool
|
|
}
|
|
if loginExpirationEnabled.Valid {
|
|
p.LoginExpirationEnabled = loginExpirationEnabled.Bool
|
|
}
|
|
if inactivityExpirationEnabled.Valid {
|
|
p.InactivityExpirationEnabled = inactivityExpirationEnabled.Bool
|
|
}
|
|
if ephemeral.Valid {
|
|
p.Ephemeral = ephemeral.Bool
|
|
}
|
|
if allowExtraDNSLabels.Valid {
|
|
p.AllowExtraDNSLabels = allowExtraDNSLabels.Bool
|
|
}
|
|
if peerStatusLastSeen.Valid {
|
|
p.Status.LastSeen = peerStatusLastSeen.Time
|
|
}
|
|
if peerStatusSessionStartedAt.Valid {
|
|
p.Status.SessionStartedAt = peerStatusSessionStartedAt.Int64
|
|
}
|
|
if peerStatusConnected.Valid {
|
|
p.Status.Connected = peerStatusConnected.Bool
|
|
}
|
|
if peerStatusLoginExpired.Valid {
|
|
p.Status.LoginExpired = peerStatusLoginExpired.Bool
|
|
}
|
|
if peerStatusRequiresApproval.Valid {
|
|
p.Status.RequiresApproval = peerStatusRequiresApproval.Bool
|
|
}
|
|
if metaHostname.Valid {
|
|
p.Meta.Hostname = metaHostname.String
|
|
}
|
|
if metaGoOS.Valid {
|
|
p.Meta.GoOS = metaGoOS.String
|
|
}
|
|
if metaKernel.Valid {
|
|
p.Meta.Kernel = metaKernel.String
|
|
}
|
|
if metaCore.Valid {
|
|
p.Meta.Core = metaCore.String
|
|
}
|
|
if metaPlatform.Valid {
|
|
p.Meta.Platform = metaPlatform.String
|
|
}
|
|
if metaOS.Valid {
|
|
p.Meta.OS = metaOS.String
|
|
}
|
|
if metaOSVersion.Valid {
|
|
p.Meta.OSVersion = metaOSVersion.String
|
|
}
|
|
if metaWtVersion.Valid {
|
|
p.Meta.WtVersion = metaWtVersion.String
|
|
}
|
|
if metaUIVersion.Valid {
|
|
p.Meta.UIVersion = metaUIVersion.String
|
|
}
|
|
if metaKernelVersion.Valid {
|
|
p.Meta.KernelVersion = metaKernelVersion.String
|
|
}
|
|
if metaSystemSerialNumber.Valid {
|
|
p.Meta.SystemSerialNumber = metaSystemSerialNumber.String
|
|
}
|
|
if metaSystemProductName.Valid {
|
|
p.Meta.SystemProductName = metaSystemProductName.String
|
|
}
|
|
if metaSystemManufacturer.Valid {
|
|
p.Meta.SystemManufacturer = metaSystemManufacturer.String
|
|
}
|
|
if locationCountryCode.Valid {
|
|
p.Location.CountryCode = locationCountryCode.String
|
|
}
|
|
if locationCityName.Valid {
|
|
p.Location.CityName = locationCityName.String
|
|
}
|
|
if locationGeoNameID.Valid {
|
|
p.Location.GeoNameID = uint(locationGeoNameID.Int64)
|
|
}
|
|
if proxyEmbedded.Valid {
|
|
p.ProxyMeta.Embedded = proxyEmbedded.Bool
|
|
}
|
|
if proxyCluster.Valid {
|
|
p.ProxyMeta.Cluster = proxyCluster.String
|
|
}
|
|
if ip != nil {
|
|
_ = json.Unmarshal(ip, &p.IP)
|
|
}
|
|
if ipv6 != nil {
|
|
_ = json.Unmarshal(ipv6, &p.IPv6)
|
|
}
|
|
if extraDNS != nil {
|
|
_ = json.Unmarshal(extraDNS, &p.ExtraDNSLabels)
|
|
}
|
|
if netAddr != nil {
|
|
_ = json.Unmarshal(netAddr, &p.Meta.NetworkAddresses)
|
|
}
|
|
if env != nil {
|
|
_ = json.Unmarshal(env, &p.Meta.Environment)
|
|
}
|
|
if flags != nil {
|
|
_ = json.Unmarshal(flags, &p.Meta.Flags)
|
|
}
|
|
if files != nil {
|
|
_ = json.Unmarshal(files, &p.Meta.Files)
|
|
}
|
|
if capabilities != nil {
|
|
_ = json.Unmarshal(capabilities, &p.Meta.Capabilities)
|
|
}
|
|
if connIP != nil {
|
|
_ = json.Unmarshal(connIP, &p.Location.ConnectionIP)
|
|
}
|
|
if metaSyncMessageVersion.Valid {
|
|
p.Meta.SyncMessageVersion = int(metaSyncMessageVersion.Int32)
|
|
}
|
|
}
|
|
return p, err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return peers, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetAccountByPeerID(ctx context.Context, peerID string) (*types.Account, error) {
|
|
var peer nbpeer.Peer
|
|
result := s.db.Select("account_id").Take(&peer, idQueryCondition, peerID)
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
|
|
}
|
|
return nil, status.NewGetAccountFromStoreError(result.Error)
|
|
}
|
|
|
|
if peer.AccountID == "" {
|
|
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
|
|
}
|
|
|
|
return s.GetAccount(ctx, peer.AccountID)
|
|
}
|
|
|
|
func (s *SqlStore) GetAccountByPeerPubKey(ctx context.Context, peerKey string) (*types.Account, error) {
|
|
var peer nbpeer.Peer
|
|
result := s.db.Select("account_id").Take(&peer, GetKeyQueryCondition(s), peerKey)
|
|
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
|
|
}
|
|
return nil, status.NewGetAccountFromStoreError(result.Error)
|
|
}
|
|
|
|
if peer.AccountID == "" {
|
|
return nil, status.Errorf(status.NotFound, "account not found: index lookup failed")
|
|
}
|
|
|
|
return s.GetAccount(ctx, peer.AccountID)
|
|
}
|
|
|
|
func (s *SqlStore) GetAccountIDByPeerPubKey(ctx context.Context, peerKey string) (string, error) {
|
|
var peer nbpeer.Peer
|
|
var accountID string
|
|
result := s.db.Model(&peer).Select("account_id").Where(GetKeyQueryCondition(s), peerKey).Take(&accountID)
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return "", status.Errorf(status.NotFound, "account not found: index lookup failed")
|
|
}
|
|
return "", status.NewGetAccountFromStoreError(result.Error)
|
|
}
|
|
|
|
return accountID, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetAccountIDByPeerID(ctx context.Context, lockStrength LockingStrength, peerID string) (string, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var accountID string
|
|
result := tx.Model(&nbpeer.Peer{}).
|
|
Select("account_id").Where(idQueryCondition, peerID).Take(&accountID)
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return "", status.Errorf(status.NotFound, "peer %s account not found", peerID)
|
|
}
|
|
return "", status.NewGetAccountFromStoreError(result.Error)
|
|
}
|
|
|
|
return accountID, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetTakenIPs(ctx context.Context, lockStrength LockingStrength, accountID string) ([]netip.Addr, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var ipJSONStrings []string
|
|
|
|
result := tx.Model(&nbpeer.Peer{}).
|
|
Where("account_id = ?", accountID).
|
|
Pluck("ip", &ipJSONStrings)
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, status.Errorf(status.NotFound, "no peers found for the account")
|
|
}
|
|
return nil, status.Errorf(status.Internal, "issue getting IPs from store: %s", result.Error)
|
|
}
|
|
|
|
ips := make([]netip.Addr, len(ipJSONStrings))
|
|
for i, ipJSON := range ipJSONStrings {
|
|
var ip netip.Addr
|
|
if err := json.Unmarshal([]byte(ipJSON), &ip); err != nil {
|
|
return nil, status.Errorf(status.Internal, "issue parsing IP JSON from store")
|
|
}
|
|
ips[i] = ip.Unmap()
|
|
}
|
|
|
|
return ips, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetPeerLabelsInAccount(ctx context.Context, lockStrength LockingStrength, accountID string, dnsLabel string) ([]string, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var labels []string
|
|
result := tx.Model(&nbpeer.Peer{}).
|
|
Where("account_id = ? AND dns_label LIKE ?", accountID, dnsLabel+"%").
|
|
Pluck("dns_label", &labels)
|
|
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, status.Errorf(status.NotFound, "no peers found for the account")
|
|
}
|
|
log.WithContext(ctx).Errorf("error when getting dns labels from the store: %s", result.Error)
|
|
return nil, status.Errorf(status.Internal, "issue getting dns labels from store: %s", result.Error)
|
|
}
|
|
|
|
return labels, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetPeerByPeerPubKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peer nbpeer.Peer
|
|
result := tx.Take(&peer, GetKeyQueryCondition(s), peerKey)
|
|
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, status.NewPeerNotFoundError(peerKey)
|
|
}
|
|
return nil, status.Errorf(status.Internal, "issue getting peer from store: %s", result.Error)
|
|
}
|
|
|
|
return &peer, nil
|
|
}
|
|
|
|
// GetAccountPeers retrieves peers for an account.
|
|
func (s *SqlStore) GetAccountPeers(ctx context.Context, lockStrength LockingStrength, accountID, nameFilter, ipFilter string) ([]*nbpeer.Peer, error) {
|
|
var peers []*nbpeer.Peer
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
query := tx.Where(accountIDCondition, accountID)
|
|
|
|
if nameFilter != "" {
|
|
query = query.Where("name LIKE ?", "%"+nameFilter+"%")
|
|
}
|
|
if ipFilter != "" {
|
|
query = query.Where("ip LIKE ? OR ipv6 LIKE ?", "%"+ipFilter+"%", "%"+ipFilter+"%")
|
|
}
|
|
|
|
if err := query.Find(&peers).Error; err != nil {
|
|
log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err)
|
|
return nil, status.Errorf(status.Internal, "failed to get peers from store")
|
|
}
|
|
|
|
return peers, nil
|
|
}
|
|
|
|
// GetUserPeers retrieves peers for a user.
|
|
func (s *SqlStore) GetUserPeers(ctx context.Context, lockStrength LockingStrength, accountID, userID string) ([]*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peers []*nbpeer.Peer
|
|
|
|
// Exclude peers added via setup keys, as they are not user-specific and have an empty user_id.
|
|
if userID == "" {
|
|
return peers, nil
|
|
}
|
|
|
|
result := tx.
|
|
Find(&peers, "account_id = ? AND user_id = ?", accountID, userID)
|
|
if err := result.Error; err != nil {
|
|
log.WithContext(ctx).Errorf("failed to get peers from the store: %s", err)
|
|
return nil, status.Errorf(status.Internal, "failed to get peers from store")
|
|
}
|
|
|
|
return peers, nil
|
|
}
|
|
|
|
func (s *SqlStore) AddPeerToAccount(ctx context.Context, peer *nbpeer.Peer) error {
|
|
if err := s.db.Create(peer).Error; err != nil {
|
|
return status.Errorf(status.Internal, "issue adding peer to account: %s", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPeerByID retrieves a peer by its ID and account ID.
|
|
func (s *SqlStore) GetPeerByID(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peer *nbpeer.Peer
|
|
result := tx.
|
|
Take(&peer, accountAndIDQueryCondition, accountID, peerID)
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, status.NewPeerNotFoundError(peerID)
|
|
}
|
|
return nil, status.Errorf(status.Internal, "failed to get peer from store")
|
|
}
|
|
|
|
return peer, nil
|
|
}
|
|
|
|
// GetPeersByIDs retrieves peers by their IDs and account ID.
|
|
func (s *SqlStore) GetPeersByIDs(ctx context.Context, lockStrength LockingStrength, accountID string, peerIDs []string) (map[string]*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peers []*nbpeer.Peer
|
|
result := tx.Find(&peers, accountAndIDsQueryCondition, accountID, peerIDs)
|
|
if result.Error != nil {
|
|
log.WithContext(ctx).Errorf("failed to get peers by ID's from the store: %s", result.Error)
|
|
return nil, status.Errorf(status.Internal, "failed to get peers by ID's from the store")
|
|
}
|
|
|
|
peersMap := make(map[string]*nbpeer.Peer)
|
|
for _, peer := range peers {
|
|
peersMap[peer.ID] = peer
|
|
}
|
|
|
|
return peersMap, nil
|
|
}
|
|
|
|
// GetAccountPeersWithExpiration retrieves a list of peers that have login expiration enabled and added by a user.
|
|
func (s *SqlStore) GetAccountPeersWithExpiration(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peers []*nbpeer.Peer
|
|
result := tx.
|
|
Where("login_expiration_enabled = ? AND peer_status_login_expired != ? AND user_id IS NOT NULL AND user_id != ''", true, true).
|
|
Find(&peers, accountIDCondition, accountID)
|
|
if err := result.Error; err != nil {
|
|
log.WithContext(ctx).Errorf("failed to get peers with expiration from the store: %s", result.Error)
|
|
return nil, status.Errorf(status.Internal, "failed to get peers with expiration from store")
|
|
}
|
|
|
|
return peers, nil
|
|
}
|
|
|
|
// GetAccountPeersWithInactivity retrieves a list of peers that have login expiration enabled and added by a user.
|
|
func (s *SqlStore) GetAccountPeersWithInactivity(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peers []*nbpeer.Peer
|
|
result := tx.
|
|
Where("inactivity_expiration_enabled = ? AND user_id IS NOT NULL AND user_id != ''", true).
|
|
Find(&peers, accountIDCondition, accountID)
|
|
if err := result.Error; err != nil {
|
|
log.WithContext(ctx).Errorf("failed to get peers with inactivity from the store: %s", result.Error)
|
|
return nil, status.Errorf(status.Internal, "failed to get peers with inactivity from store")
|
|
}
|
|
|
|
return peers, nil
|
|
}
|
|
|
|
// GetAllEphemeralPeers retrieves all peers with Ephemeral set to true across all accounts, optimized for batch processing.
|
|
func (s *SqlStore) GetAllEphemeralPeers(ctx context.Context, lockStrength LockingStrength) ([]*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var allEphemeralPeers, batchPeers []*nbpeer.Peer
|
|
result := tx.
|
|
Where("ephemeral = ?", true).
|
|
FindInBatches(&batchPeers, 1000, func(tx *gorm.DB, batch int) error {
|
|
allEphemeralPeers = append(allEphemeralPeers, batchPeers...)
|
|
return nil
|
|
})
|
|
|
|
if result.Error != nil {
|
|
log.WithContext(ctx).Errorf("failed to retrieve ephemeral peers: %s", result.Error)
|
|
return nil, fmt.Errorf("failed to retrieve ephemeral peers")
|
|
}
|
|
|
|
return allEphemeralPeers, nil
|
|
}
|
|
|
|
// DeletePeer removes a peer from the store.
|
|
func (s *SqlStore) DeletePeer(ctx context.Context, accountID string, peerID string) error {
|
|
result := s.db.Delete(&nbpeer.Peer{}, accountAndIDQueryCondition, accountID, peerID)
|
|
if err := result.Error; err != nil {
|
|
log.WithContext(ctx).Errorf("failed to delete peer from the store: %s", err)
|
|
return status.Errorf(status.Internal, "failed to delete peer from store")
|
|
}
|
|
|
|
if result.RowsAffected == 0 {
|
|
return status.NewPeerNotFoundError(peerID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength, accountID string, ip net.IP) (*nbpeer.Peer, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
column := "ip"
|
|
if ip.To4() == nil {
|
|
column = "ipv6"
|
|
}
|
|
jsonValue := fmt.Sprintf(`"%s"`, ip.String())
|
|
|
|
var peer nbpeer.Peer
|
|
result := tx.
|
|
Take(&peer, fmt.Sprintf("account_id = ? AND %s = ?", column), accountID, jsonValue)
|
|
if result.Error != nil {
|
|
// A tunnel-IP miss is an expected outcome (e.g. the proxy's
|
|
// ValidateTunnelPeer probing an address that isn't in the
|
|
// account roster); surface it as NotFound so callers can tell
|
|
// it apart from a real store failure.
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return nil, status.Errorf(status.NotFound, "peer with ip %s not found", ip.String())
|
|
}
|
|
return nil, status.Errorf(status.Internal, "failed to get peer from store")
|
|
}
|
|
|
|
return &peer, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetPeerIdByLabel(ctx context.Context, lockStrength LockingStrength, accountID string, hostname string) (string, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peerID string
|
|
result := tx.Model(&nbpeer.Peer{}).
|
|
Select("id").
|
|
// Where(" = ?", hostname).
|
|
Where("account_id = ? AND dns_label = ?", accountID, hostname).
|
|
Limit(1).
|
|
Scan(&peerID)
|
|
|
|
if peerID == "" {
|
|
return "", gorm.ErrRecordNotFound
|
|
}
|
|
|
|
return peerID, result.Error
|
|
}
|
|
|
|
// GetEmbeddedProxyPeerIDsByCluster returns peer IDs of all embedded proxy peers
|
|
// in the account, grouped by their ProxyCluster. The map is nil when no embedded
|
|
// proxy peers exist.
|
|
func (s *SqlStore) GetEmbeddedProxyPeerIDsByCluster(ctx context.Context, accountID string) (map[string][]string, error) {
|
|
type row struct {
|
|
ID string
|
|
Cluster string
|
|
}
|
|
var rows []row
|
|
result := s.db.Model(&nbpeer.Peer{}).
|
|
Select("id, proxy_meta_cluster AS cluster").
|
|
Where("account_id = ? AND proxy_meta_embedded = ?", accountID, true).
|
|
Scan(&rows)
|
|
if result.Error != nil {
|
|
return nil, status.Errorf(status.Internal, "failed to get embedded proxy peers: %s", result.Error)
|
|
}
|
|
|
|
out := make(map[string][]string, len(rows))
|
|
for _, r := range rows {
|
|
out[r.Cluster] = append(out[r.Cluster], r.ID)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetUserIDByPeerKey(ctx context.Context, lockStrength LockingStrength, peerKey string) (string, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var userID string
|
|
result := tx.Model(&nbpeer.Peer{}).
|
|
Select("user_id").
|
|
Take(&userID, GetKeyQueryCondition(s), peerKey)
|
|
|
|
if result.Error != nil {
|
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
return "", status.Errorf(status.NotFound, "peer not found: index lookup failed")
|
|
}
|
|
return "", status.Errorf(status.Internal, "failed to get user ID by peer key")
|
|
}
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
func (s *SqlStore) GetPeerIDByKey(ctx context.Context, lockStrength LockingStrength, key string) (string, error) {
|
|
tx := s.db
|
|
if lockStrength != LockingStrengthNone {
|
|
tx = tx.Clauses(clause.Locking{Strength: string(lockStrength)})
|
|
}
|
|
|
|
var peerID string
|
|
result := tx.Model(&nbpeer.Peer{}).
|
|
Select("id").
|
|
Where(GetKeyQueryCondition(s), key).
|
|
Limit(1).
|
|
Scan(&peerID)
|
|
if result.Error != nil {
|
|
log.WithContext(ctx).Errorf("failed to get peer ID by key: %s", result.Error)
|
|
return "", status.Errorf(status.Internal, "failed to get peer ID by key")
|
|
}
|
|
|
|
return peerID, nil
|
|
}
|