mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-25 09:33:24 +01:00
49e65109d2
Implemented inactivity expiration by checking the status of a peer: after a configurable period of time following netbird down, the peer shows login required.
263 lines
7.7 KiB
Go
263 lines
7.7 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/xid"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
nbgroup "github.com/netbirdio/netbird/management/server/group"
|
|
nbpeer "github.com/netbirdio/netbird/management/server/peer"
|
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
"github.com/netbirdio/netbird/util"
|
|
)
|
|
|
|
// storeFileName Store file name. Stored in the datadir
|
|
const storeFileName = "store.json"
|
|
|
|
// FileStore represents an account storage backed by a file persisted to disk
|
|
type FileStore struct {
|
|
Accounts map[string]*Account
|
|
SetupKeyID2AccountID map[string]string `json:"-"`
|
|
PeerKeyID2AccountID map[string]string `json:"-"`
|
|
PeerID2AccountID map[string]string `json:"-"`
|
|
UserID2AccountID map[string]string `json:"-"`
|
|
PrivateDomain2AccountID map[string]string `json:"-"`
|
|
HashedPAT2TokenID map[string]string `json:"-"`
|
|
TokenID2UserID map[string]string `json:"-"`
|
|
InstallationID string
|
|
|
|
// mutex to synchronise Store read/write operations
|
|
mux sync.Mutex `json:"-"`
|
|
storeFile string `json:"-"`
|
|
|
|
metrics telemetry.AppMetrics `json:"-"`
|
|
}
|
|
|
|
// NewFileStore restores a store from the file located in the datadir
|
|
func NewFileStore(ctx context.Context, dataDir string, metrics telemetry.AppMetrics) (*FileStore, error) {
|
|
fs, err := restore(ctx, filepath.Join(dataDir, storeFileName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fs.metrics = metrics
|
|
return fs, nil
|
|
}
|
|
|
|
// restore the state of the store from the file.
|
|
// Creates a new empty store file if doesn't exist
|
|
func restore(ctx context.Context, file string) (*FileStore, error) {
|
|
if _, err := os.Stat(file); os.IsNotExist(err) {
|
|
// create a new FileStore if previously didn't exist (e.g. first run)
|
|
s := &FileStore{
|
|
Accounts: make(map[string]*Account),
|
|
mux: sync.Mutex{},
|
|
SetupKeyID2AccountID: make(map[string]string),
|
|
PeerKeyID2AccountID: make(map[string]string),
|
|
UserID2AccountID: make(map[string]string),
|
|
PrivateDomain2AccountID: make(map[string]string),
|
|
PeerID2AccountID: make(map[string]string),
|
|
HashedPAT2TokenID: make(map[string]string),
|
|
TokenID2UserID: make(map[string]string),
|
|
storeFile: file,
|
|
}
|
|
|
|
err = s.persist(ctx, file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
read, err := util.ReadJson(file, &FileStore{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
store := read.(*FileStore)
|
|
store.storeFile = file
|
|
store.SetupKeyID2AccountID = make(map[string]string)
|
|
store.PeerKeyID2AccountID = make(map[string]string)
|
|
store.UserID2AccountID = make(map[string]string)
|
|
store.PrivateDomain2AccountID = make(map[string]string)
|
|
store.PeerID2AccountID = make(map[string]string)
|
|
store.HashedPAT2TokenID = make(map[string]string)
|
|
store.TokenID2UserID = make(map[string]string)
|
|
|
|
for accountID, account := range store.Accounts {
|
|
if account.Settings == nil {
|
|
account.Settings = &Settings{
|
|
PeerLoginExpirationEnabled: false,
|
|
PeerLoginExpiration: DefaultPeerLoginExpiration,
|
|
|
|
PeerInactivityExpirationEnabled: false,
|
|
PeerInactivityExpiration: DefaultPeerInactivityExpiration,
|
|
}
|
|
}
|
|
|
|
for setupKeyId := range account.SetupKeys {
|
|
store.SetupKeyID2AccountID[strings.ToUpper(setupKeyId)] = accountID
|
|
}
|
|
|
|
for _, peer := range account.Peers {
|
|
store.PeerKeyID2AccountID[peer.Key] = accountID
|
|
store.PeerID2AccountID[peer.ID] = accountID
|
|
}
|
|
for _, user := range account.Users {
|
|
store.UserID2AccountID[user.Id] = accountID
|
|
if user.Issued == "" {
|
|
user.Issued = UserIssuedAPI
|
|
account.Users[user.Id] = user
|
|
}
|
|
|
|
for _, pat := range user.PATs {
|
|
store.TokenID2UserID[pat.ID] = user.Id
|
|
store.HashedPAT2TokenID[pat.HashedToken] = pat.ID
|
|
}
|
|
}
|
|
|
|
if account.Domain != "" && account.DomainCategory == PrivateCategory &&
|
|
account.IsDomainPrimaryAccount {
|
|
store.PrivateDomain2AccountID[account.Domain] = accountID
|
|
}
|
|
|
|
// TODO: delete this block after migration
|
|
policies := make(map[string]int, len(account.Policies))
|
|
for i, policy := range account.Policies {
|
|
policies[policy.ID] = i
|
|
policy.UpgradeAndFix()
|
|
}
|
|
if account.Policies == nil {
|
|
account.Policies = make([]*Policy, 0)
|
|
}
|
|
|
|
// for data migration. Can be removed once most base will be with labels
|
|
existingLabels := account.getPeerDNSLabels()
|
|
if len(existingLabels) != len(account.Peers) {
|
|
addPeerLabelsToAccount(ctx, account, existingLabels)
|
|
}
|
|
|
|
// TODO: delete this block after migration
|
|
// Set API as issuer for groups which has not this field
|
|
for _, group := range account.Groups {
|
|
if group.Issued == "" {
|
|
group.Issued = nbgroup.GroupIssuedAPI
|
|
}
|
|
}
|
|
|
|
allGroup, err := account.GetGroupAll()
|
|
if err != nil {
|
|
log.WithContext(ctx).Errorf("unable to find the All group, this should happen only when migrate from a version that didn't support groups. Error: %v", err)
|
|
// if the All group didn't exist we probably don't have routes to update
|
|
continue
|
|
}
|
|
|
|
for _, route := range account.Routes {
|
|
if len(route.Groups) == 0 {
|
|
route.Groups = []string{allGroup.ID}
|
|
}
|
|
}
|
|
|
|
// migration to Peer.ID from Peer.Key.
|
|
// Old peers that require migration have an empty Peer.ID in the store.json.
|
|
// Generate new ID with xid for these peers.
|
|
// Set the Peer.ID to the newly generated value.
|
|
// Replace all the mentions of Peer.Key as ID (groups and routes).
|
|
// Swap Peer.Key with Peer.ID in the Account.Peers map.
|
|
migrationPeers := make(map[string]*nbpeer.Peer) // key to Peer
|
|
for key, peer := range account.Peers {
|
|
// set LastLogin for the peers that were onboarded before the peer login expiration feature
|
|
if peer.LastLogin.IsZero() {
|
|
peer.LastLogin = time.Now().UTC()
|
|
}
|
|
if peer.ID != "" {
|
|
continue
|
|
}
|
|
id := xid.New().String()
|
|
peer.ID = id
|
|
migrationPeers[key] = peer
|
|
}
|
|
|
|
if len(migrationPeers) > 0 {
|
|
// swap Peer.Key with Peer.ID in the Account.Peers map.
|
|
for key, peer := range migrationPeers {
|
|
delete(account.Peers, key)
|
|
account.Peers[peer.ID] = peer
|
|
store.PeerID2AccountID[peer.ID] = accountID
|
|
}
|
|
|
|
// detect groups that have Peer.Key as a reference and replace it with ID.
|
|
for _, group := range account.Groups {
|
|
for i, peer := range group.Peers {
|
|
if p, ok := migrationPeers[peer]; ok {
|
|
group.Peers[i] = p.ID
|
|
}
|
|
}
|
|
}
|
|
|
|
// detect routes that have Peer.Key as a reference and replace it with ID.
|
|
for _, route := range account.Routes {
|
|
if peer, ok := migrationPeers[route.Peer]; ok {
|
|
route.Peer = peer.ID
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// we need this persist to apply changes we made to account.Peers (we set them to Disconnected)
|
|
err = store.persist(ctx, store.storeFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return store, nil
|
|
}
|
|
|
|
// persist account data to a file
|
|
// It is recommended to call it with locking FileStore.mux
|
|
func (s *FileStore) persist(ctx context.Context, file string) error {
|
|
start := time.Now()
|
|
err := util.WriteJson(file, s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
took := time.Since(start)
|
|
if s.metrics != nil {
|
|
s.metrics.StoreMetrics().CountPersistenceDuration(took)
|
|
}
|
|
log.WithContext(ctx).Debugf("took %d ms to persist the FileStore", took.Milliseconds())
|
|
return nil
|
|
}
|
|
|
|
// GetAllAccounts returns all accounts
|
|
func (s *FileStore) GetAllAccounts(_ context.Context) (all []*Account) {
|
|
s.mux.Lock()
|
|
defer s.mux.Unlock()
|
|
for _, a := range s.Accounts {
|
|
all = append(all, a.Copy())
|
|
}
|
|
|
|
return all
|
|
}
|
|
|
|
// Close the FileStore persisting data to disk
|
|
func (s *FileStore) Close(ctx context.Context) error {
|
|
s.mux.Lock()
|
|
defer s.mux.Unlock()
|
|
|
|
log.WithContext(ctx).Infof("closing FileStore")
|
|
|
|
return s.persist(ctx, s.storeFile)
|
|
}
|
|
|
|
// GetStoreEngine returns FileStoreEngine
|
|
func (s *FileStore) GetStoreEngine() StoreEngine {
|
|
return FileStoreEngine
|
|
}
|