mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-27 01:51:30 +02:00
The dashboard needs to know which peers have opted out of remote jobs so it can reflect that in the UI, the same way it surfaces the SSH server flag. Report RemoteJobsAllowed as peer system-info: the client sets it on the reported flags (like ServerSSHAllowed), the proto Flags message carries it, and management decodes it onto the peer meta and exposes it on the peers API as remote_jobs_allowed. Kept out of the components/network-map path: unlike ServerSSHAllowed it does not participate in firewall-rule calculation, so it only rides the reporting flags, not ComponentPeer.
616 lines
21 KiB
Go
616 lines
21 KiB
Go
package peer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/management/server/util"
|
|
"github.com/netbirdio/netbird/shared/management/http/api"
|
|
sharedTypes "github.com/netbirdio/netbird/shared/management/types"
|
|
)
|
|
|
|
// Peer capability constants mirror the proto enum values.
|
|
const (
|
|
PeerCapabilitySourcePrefixes int32 = 1
|
|
PeerCapabilityIPv6Overlay int32 = 2
|
|
PeerCapabilityComponentNetworkMap int32 = 3
|
|
)
|
|
|
|
// Peer represents a machine connected to the network.
|
|
// The Peer is a WireGuard peer identified by a public key
|
|
type Peer struct {
|
|
// ID is an internal ID of the peer
|
|
ID string `gorm:"primaryKey"`
|
|
// AccountID is a reference to Account that this object belongs
|
|
AccountID string `json:"-" gorm:"index"`
|
|
// WireGuard public key
|
|
Key string // uniqueness index (check migrations)
|
|
// IP address of the Peer
|
|
IP netip.Addr `gorm:"serializer:json"` // uniqueness index per accountID (check migrations)
|
|
// IPv6 overlay address of the Peer, zero value if IPv6 is not enabled for the account.
|
|
IPv6 netip.Addr `gorm:"serializer:json"`
|
|
// Meta is a Peer system meta data
|
|
Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"`
|
|
// ProxyMeta is metadata related to proxy peers
|
|
ProxyMeta ProxyMeta `gorm:"embedded;embeddedPrefix:proxy_meta_"`
|
|
// Name is peer's name (machine name)
|
|
Name string `gorm:"index"`
|
|
// DNSLabel is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's
|
|
// domain to the peer label. e.g. peer-dns-label.netbird.cloud
|
|
DNSLabel string // uniqueness index per accountID (check migrations)
|
|
// Status peer's management connection status
|
|
Status *PeerStatus `gorm:"embedded;embeddedPrefix:peer_status_"`
|
|
// The user ID that registered the peer
|
|
UserID string
|
|
// SSHKey is a public SSH key of the peer
|
|
SSHKey string
|
|
// SSHEnabled indicates whether SSH server is enabled on the peer
|
|
SSHEnabled bool
|
|
// LoginExpirationEnabled indicates whether peer's login expiration is enabled and once expired the peer has to re-login.
|
|
// Works with LastLogin
|
|
LoginExpirationEnabled bool
|
|
|
|
InactivityExpirationEnabled bool
|
|
// LastLogin the time when peer performed last login operation
|
|
LastLogin *time.Time
|
|
// CreatedAt records the time the peer was created
|
|
CreatedAt time.Time
|
|
// Indicate ephemeral peer attribute
|
|
Ephemeral bool `gorm:"index"`
|
|
|
|
// Geo location based on connection IP
|
|
Location Location `gorm:"embedded;embeddedPrefix:location_"`
|
|
|
|
// ExtraDNSLabels is a list of additional DNS labels that can be used to resolve the peer
|
|
ExtraDNSLabels []string `gorm:"serializer:json"`
|
|
// AllowExtraDNSLabels indicates whether the peer allows extra DNS labels to be used for resolving the peer
|
|
AllowExtraDNSLabels bool
|
|
}
|
|
|
|
type ProxyMeta struct {
|
|
Embedded bool `gorm:"index"`
|
|
Cluster string `gorm:"index"`
|
|
}
|
|
|
|
type PeerStatus struct { //nolint:revive
|
|
// LastSeen is the last time the peer status was updated (i.e. the last
|
|
// time we observed the peer being alive on a sync stream). Written by
|
|
// the database (CURRENT_TIMESTAMP) — callers do not supply it.
|
|
LastSeen time.Time
|
|
// SessionStartedAt records when the currently-active sync stream began,
|
|
// stored as Unix nanoseconds. It acts as the optimistic-locking token
|
|
// for status updates: a stream is only allowed to mutate the peer's
|
|
// status when its own token strictly exceeds the stored token (when connecting)
|
|
// or matches it exactly (for disconnects). Zero means "no
|
|
// active session". Integer nanoseconds are used so equality is
|
|
// precision-safe across drivers, and so the predicates compose to a
|
|
// single bigint comparison.
|
|
SessionStartedAt int64 `gorm:"not null;default:0"`
|
|
// Connected indicates whether peer is connected to the management service or not
|
|
Connected bool
|
|
// LoginExpired
|
|
LoginExpired bool
|
|
// RequiresApproval indicates whether peer requires approval or not
|
|
RequiresApproval bool
|
|
}
|
|
|
|
// Location is a geo location information of a Peer based on public connection IP
|
|
type Location struct {
|
|
ConnectionIP net.IP `gorm:"serializer:json"` // from grpc peer or reverse proxy headers depends on setup
|
|
CountryCode string
|
|
CityName string
|
|
GeoNameID uint // city level geoname id
|
|
}
|
|
|
|
// equal reports whether two locations match. ConnectionIP is a net.IP slice, so it uses
|
|
// IP.Equal, not ==.
|
|
func (l Location) equal(other Location) bool {
|
|
return l.CountryCode == other.CountryCode &&
|
|
l.CityName == other.CityName &&
|
|
l.GeoNameID == other.GeoNameID &&
|
|
l.ConnectionIP.Equal(other.ConnectionIP)
|
|
}
|
|
|
|
// NetworkAddress is the IP address with network and MAC address of a network interface
|
|
type NetworkAddress struct {
|
|
NetIP netip.Prefix `gorm:"serializer:json"`
|
|
Mac string
|
|
}
|
|
|
|
// Environment is a system environment information
|
|
type Environment struct {
|
|
Cloud string
|
|
Platform string
|
|
}
|
|
|
|
// File is a file on the system.
|
|
type File struct {
|
|
Path string
|
|
Exist bool
|
|
ProcessIsRunning bool
|
|
}
|
|
|
|
// Flags defines a set of options to control feature behavior
|
|
type Flags struct {
|
|
RosenpassEnabled bool
|
|
RosenpassPermissive bool
|
|
ServerSSHAllowed bool
|
|
RemoteJobsAllowed bool
|
|
|
|
DisableClientRoutes bool
|
|
DisableServerRoutes bool
|
|
DisableDNS bool
|
|
DisableFirewall bool
|
|
BlockLANAccess bool
|
|
BlockInbound bool
|
|
DisableIPv6 bool
|
|
|
|
LazyConnectionEnabled bool
|
|
}
|
|
|
|
// PeerSystemMeta is a metadata of a Peer machine system
|
|
type PeerSystemMeta struct { //nolint:revive
|
|
Hostname string
|
|
GoOS string
|
|
Kernel string
|
|
Core string
|
|
Platform string
|
|
OS string
|
|
OSVersion string
|
|
WtVersion string
|
|
UIVersion string
|
|
KernelVersion string
|
|
NetworkAddresses []NetworkAddress `gorm:"serializer:json"`
|
|
SystemSerialNumber string
|
|
SystemProductName string
|
|
SystemManufacturer string
|
|
Environment Environment `gorm:"serializer:json"`
|
|
Flags Flags `gorm:"serializer:json"`
|
|
Files []File `gorm:"serializer:json"`
|
|
Capabilities []int32 `gorm:"serializer:json"`
|
|
SyncMessageVersion int
|
|
}
|
|
|
|
func (p PeerSystemMeta) isEqual(other PeerSystemMeta) bool {
|
|
return len(metaDiff(p, other)) == 0
|
|
}
|
|
|
|
func (p PeerSystemMeta) isEmpty() bool {
|
|
return p.Hostname == "" &&
|
|
p.GoOS == "" &&
|
|
p.Kernel == "" &&
|
|
p.Core == "" &&
|
|
p.Platform == "" &&
|
|
p.OS == "" &&
|
|
p.OSVersion == "" &&
|
|
p.WtVersion == "" &&
|
|
p.UIVersion == "" &&
|
|
p.KernelVersion == "" &&
|
|
len(p.NetworkAddresses) == 0 &&
|
|
p.SystemSerialNumber == "" &&
|
|
p.SystemProductName == "" &&
|
|
p.SystemManufacturer == "" &&
|
|
p.Environment.Cloud == "" &&
|
|
p.Environment.Platform == "" &&
|
|
len(p.Files) == 0
|
|
}
|
|
|
|
// AddedWithSSOLogin indicates whether this peer has been added with an SSO login by a user.
|
|
func (p *Peer) AddedWithSSOLogin() bool {
|
|
return p.UserID != ""
|
|
}
|
|
|
|
// ToComponent converts the peer to its self-contained components
|
|
// representation, carrying exactly the subset of peer data that crosses the
|
|
// components wire format. Returns nil for a nil peer so callers can convert
|
|
// possibly-missing peers without guarding.
|
|
func (p *Peer) ToComponent() *sharedTypes.ComponentPeer {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
cp := &sharedTypes.ComponentPeer{
|
|
ID: p.ID,
|
|
Key: p.Key,
|
|
IP: p.IP,
|
|
IPv6: p.IPv6,
|
|
DNSLabel: p.DNSLabel,
|
|
SSHKey: p.SSHKey,
|
|
SSHEnabled: p.SSHEnabled,
|
|
ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed,
|
|
AgentVersion: p.Meta.WtVersion,
|
|
SupportsSourcePrefixes: p.SupportsSourcePrefixes(),
|
|
SupportsIPv6: p.SupportsIPv6(),
|
|
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
|
AddedWithSSOLogin: p.AddedWithSSOLogin(),
|
|
}
|
|
if p.LastLogin != nil {
|
|
cp.LastLogin = *p.LastLogin
|
|
}
|
|
return cp
|
|
}
|
|
|
|
// HasCapability reports whether the peer has the given capability.
|
|
func (p *Peer) HasCapability(capability int32) bool {
|
|
return slices.Contains(p.Meta.Capabilities, capability)
|
|
}
|
|
|
|
// SupportsIPv6 reports whether the peer supports IPv6 overlay.
|
|
func (p *Peer) SupportsIPv6() bool {
|
|
return !p.Meta.Flags.DisableIPv6 && p.HasCapability(PeerCapabilityIPv6Overlay)
|
|
}
|
|
|
|
// SupportsSourcePrefixes reports whether the peer reads SourcePrefixes.
|
|
func (p *Peer) SupportsSourcePrefixes() bool {
|
|
return p.HasCapability(PeerCapabilitySourcePrefixes)
|
|
}
|
|
|
|
// SupportsComponentNetworkMap reports whether the peer assembles its
|
|
// NetworkMap from server-shipped components instead of consuming a fully
|
|
// expanded NetworkMap. Determines whether the network_map controller skips
|
|
// Calculate() server-side and emits the components envelope.
|
|
func (p *Peer) SupportsComponentNetworkMap() bool {
|
|
return p.HasCapability(PeerCapabilityComponentNetworkMap)
|
|
}
|
|
|
|
func capabilitiesEqual(a, b []int32) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
set := make(map[int32]struct{}, len(a))
|
|
for _, c := range a {
|
|
set[c] = struct{}{}
|
|
}
|
|
for _, c := range b {
|
|
if _, ok := set[c]; !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Copy copies Peer object
|
|
func (p *Peer) Copy() *Peer {
|
|
peerStatus := p.Status
|
|
if peerStatus != nil {
|
|
peerStatus = p.Status.Copy()
|
|
}
|
|
return &Peer{
|
|
ID: p.ID,
|
|
AccountID: p.AccountID,
|
|
Key: p.Key,
|
|
IP: p.IP,
|
|
IPv6: p.IPv6,
|
|
Meta: p.Meta,
|
|
Name: p.Name,
|
|
DNSLabel: p.DNSLabel,
|
|
Status: peerStatus,
|
|
UserID: p.UserID,
|
|
SSHKey: p.SSHKey,
|
|
SSHEnabled: p.SSHEnabled,
|
|
LoginExpirationEnabled: p.LoginExpirationEnabled,
|
|
LastLogin: p.LastLogin,
|
|
CreatedAt: p.CreatedAt,
|
|
Ephemeral: p.Ephemeral,
|
|
ProxyMeta: p.ProxyMeta,
|
|
Location: p.Location,
|
|
InactivityExpirationEnabled: p.InactivityExpirationEnabled,
|
|
ExtraDNSLabels: slices.Clone(p.ExtraDNSLabels),
|
|
AllowExtraDNSLabels: p.AllowExtraDNSLabels,
|
|
}
|
|
}
|
|
|
|
// UpdateMetaIfNew updates peer's system metadata and connection geo location if
|
|
// new information is provided. newLocation is the geo location resolved from the
|
|
// peer's current connection IP, or nil when there is nothing to apply (geo
|
|
// disabled, no real IP, or the IP is unchanged); the caller owns the expensive
|
|
// lookup and the same-IP guard. It returns a MetaDiff describing what changed;
|
|
// diff.Updated() reports whether the peer needs to be persisted.
|
|
func (p *Peer) UpdateMetaIfNew(ctx context.Context, meta PeerSystemMeta, newLocation *Location) MetaDiff {
|
|
if meta.isEmpty() {
|
|
return MetaDiff{}
|
|
}
|
|
|
|
// Avoid overwriting UIVersion if the update was triggered sole by the CLI client
|
|
if meta.UIVersion == "" {
|
|
meta.UIVersion = p.Meta.UIVersion
|
|
}
|
|
|
|
effectiveLocation := p.Location
|
|
if newLocation != nil {
|
|
effectiveLocation = *newLocation
|
|
}
|
|
|
|
diff := diffMeta(p.Meta, meta, p.Location, effectiveLocation)
|
|
if diff.Updated() {
|
|
p.Meta = meta
|
|
}
|
|
p.Location = effectiveLocation
|
|
|
|
if diff.Updated() {
|
|
log.WithContext(ctx).Debug(diff.LogSummary())
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
// MetaDiff holds a peer's full before/after state across a sync: both metas and both
|
|
// connection locations (the location lives on Peer, not PeerSystemMeta, but posture
|
|
// checks read it). Changed lists what moved, for logging and the persistence decision;
|
|
// the snapshots let a posture check be replayed against old and new. Everything is derived
|
|
// from these fields, so there are no parallel per-field flags to keep in sync.
|
|
type MetaDiff struct {
|
|
OldMeta PeerSystemMeta
|
|
NewMeta PeerSystemMeta
|
|
OldLocation Location
|
|
NewLocation Location
|
|
|
|
Changed []string
|
|
}
|
|
|
|
// Updated reports whether anything changed and the peer must be persisted. diffMeta fills
|
|
// Changed in the pass that builds the diff, so this is a length check, not a re-comparison.
|
|
// Pointer receiver: MetaDiff embeds two metas, so copying it per call is wasteful.
|
|
func (d *MetaDiff) Updated() bool {
|
|
return len(d.Changed) != 0
|
|
}
|
|
|
|
// VersionChanged reports whether the WireGuard client version changed (a client upgrade).
|
|
func (d *MetaDiff) VersionChanged() bool {
|
|
return d.OldMeta.WtVersion != d.NewMeta.WtVersion
|
|
}
|
|
|
|
// HostnameChanged reports whether the peer's hostname changed.
|
|
func (d *MetaDiff) HostnameChanged() bool {
|
|
return d.OldMeta.Hostname != d.NewMeta.Hostname
|
|
}
|
|
|
|
// LogSummary renders the changed fields as a single human-readable line.
|
|
func (d *MetaDiff) LogSummary() string {
|
|
return fmt.Sprintf("peer meta updated, %d field(s) changed: %s",
|
|
len(d.Changed), strings.Join(d.Changed, ", "))
|
|
}
|
|
|
|
func metaDiff(oldMeta, newMeta PeerSystemMeta) []string {
|
|
return diffMeta(oldMeta, newMeta, Location{}, Location{}).Changed
|
|
}
|
|
|
|
// diffMeta snapshots a peer's old and new state and records a Changed entry per field that
|
|
// moved. It is the single source of truth for the comparison: isEqual is an empty Changed
|
|
// list, so the log line and the persistence decision can never disagree.
|
|
func diffMeta(oldMeta, newMeta PeerSystemMeta, oldLocation, newLocation Location) MetaDiff {
|
|
d := MetaDiff{OldMeta: oldMeta, NewMeta: newMeta, OldLocation: oldLocation, NewLocation: newLocation}
|
|
add := func(field string, oldVal, newVal any) {
|
|
d.Changed = append(d.Changed, fmt.Sprintf("%s: %v -> %v", field, oldVal, newVal))
|
|
}
|
|
|
|
if oldMeta.Hostname != newMeta.Hostname {
|
|
add("hostname", oldMeta.Hostname, newMeta.Hostname)
|
|
}
|
|
if oldMeta.GoOS != newMeta.GoOS {
|
|
add("goos", oldMeta.GoOS, newMeta.GoOS)
|
|
}
|
|
if oldMeta.Kernel != newMeta.Kernel {
|
|
add("kernel", oldMeta.Kernel, newMeta.Kernel)
|
|
}
|
|
if oldMeta.KernelVersion != newMeta.KernelVersion {
|
|
add("kernel_version", oldMeta.KernelVersion, newMeta.KernelVersion)
|
|
}
|
|
if oldMeta.Core != newMeta.Core {
|
|
add("core", oldMeta.Core, newMeta.Core)
|
|
}
|
|
if oldMeta.Platform != newMeta.Platform {
|
|
add("platform", oldMeta.Platform, newMeta.Platform)
|
|
}
|
|
if oldMeta.OS != newMeta.OS {
|
|
add("os", oldMeta.OS, newMeta.OS)
|
|
}
|
|
if oldMeta.OSVersion != newMeta.OSVersion {
|
|
add("os_version", oldMeta.OSVersion, newMeta.OSVersion)
|
|
}
|
|
if oldMeta.WtVersion != newMeta.WtVersion {
|
|
add("wt_version", oldMeta.WtVersion, newMeta.WtVersion)
|
|
}
|
|
if oldMeta.UIVersion != newMeta.UIVersion {
|
|
add("ui_version", oldMeta.UIVersion, newMeta.UIVersion)
|
|
}
|
|
if oldMeta.SystemSerialNumber != newMeta.SystemSerialNumber {
|
|
add("system_serial_number", oldMeta.SystemSerialNumber, newMeta.SystemSerialNumber)
|
|
}
|
|
if oldMeta.SystemProductName != newMeta.SystemProductName {
|
|
add("system_product_name", oldMeta.SystemProductName, newMeta.SystemProductName)
|
|
}
|
|
if oldMeta.SystemManufacturer != newMeta.SystemManufacturer {
|
|
add("system_manufacturer", oldMeta.SystemManufacturer, newMeta.SystemManufacturer)
|
|
}
|
|
if oldMeta.Environment.Cloud != newMeta.Environment.Cloud {
|
|
add("environment_cloud", oldMeta.Environment.Cloud, newMeta.Environment.Cloud)
|
|
}
|
|
if oldMeta.Environment.Platform != newMeta.Environment.Platform {
|
|
add("environment_platform", oldMeta.Environment.Platform, newMeta.Environment.Platform)
|
|
}
|
|
if !oldMeta.Flags.isEqual(newMeta.Flags) {
|
|
add("flags", fmt.Sprintf("%+v", oldMeta.Flags), fmt.Sprintf("%+v", newMeta.Flags))
|
|
}
|
|
if !capabilitiesEqual(oldMeta.Capabilities, newMeta.Capabilities) {
|
|
add("capabilities", oldMeta.Capabilities, newMeta.Capabilities)
|
|
}
|
|
if !sameMultiset(oldMeta.NetworkAddresses, newMeta.NetworkAddresses) {
|
|
add("network_addresses", fmt.Sprintf("%v", oldMeta.NetworkAddresses), fmt.Sprintf("%v", newMeta.NetworkAddresses))
|
|
}
|
|
if !sameMultiset(oldMeta.Files, newMeta.Files) {
|
|
add("files", fmt.Sprintf("%v", oldMeta.Files), fmt.Sprintf("%v", newMeta.Files))
|
|
}
|
|
if oldMeta.SyncMessageVersion != newMeta.SyncMessageVersion {
|
|
add("sync_meta_version", fmt.Sprintf("%d", oldMeta.SyncMessageVersion), fmt.Sprintf("%d", newMeta.SyncMessageVersion))
|
|
}
|
|
|
|
if !oldLocation.equal(newLocation) {
|
|
add("connection_ip", oldLocation.ConnectionIP, newLocation.ConnectionIP)
|
|
}
|
|
|
|
return d
|
|
}
|
|
|
|
// sameMultiset reports whether two slices contain the same elements with the
|
|
// same multiplicity, ignoring order. The element type is the comparison key, so
|
|
// every field participates in equality.
|
|
func sameMultiset[T comparable](a, b []T) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
counts := make(map[T]int, len(a))
|
|
for _, v := range a {
|
|
counts[v]++
|
|
}
|
|
for _, v := range b {
|
|
counts[v]--
|
|
if counts[v] == 0 {
|
|
delete(counts, v)
|
|
}
|
|
}
|
|
return len(counts) == 0
|
|
}
|
|
|
|
// GetLastLogin returns the last login time of the peer.
|
|
func (p *Peer) GetLastLogin() time.Time {
|
|
if p.LastLogin != nil {
|
|
return *p.LastLogin
|
|
}
|
|
return time.Time{}
|
|
|
|
}
|
|
|
|
// MarkLoginExpired marks peer's status expired or not
|
|
func (p *Peer) MarkLoginExpired(expired bool) {
|
|
newStatus := p.Status.Copy()
|
|
newStatus.LoginExpired = expired
|
|
if expired {
|
|
newStatus.Connected = false
|
|
}
|
|
p.Status = newStatus
|
|
}
|
|
|
|
// SessionExpired indicates whether the peer's session has expired or not.
|
|
// If Peer.LastLogin plus the expiresIn duration has happened already; then session has expired.
|
|
// Return true if a session has expired, false otherwise, and time left to expiration (negative when expired).
|
|
// Session expiration can be disabled/enabled on a Peer level via Peer.LoginExpirationEnabled property.
|
|
// Session expiration can also be disabled/enabled globally on the Account level via Settings.PeerLoginExpirationEnabled.
|
|
// Only peers added by interactive SSO login can be expired.
|
|
func (p *Peer) SessionExpired(expiresIn time.Duration) (bool, time.Duration) {
|
|
if !p.AddedWithSSOLogin() || !p.InactivityExpirationEnabled || p.Status.Connected {
|
|
return false, 0
|
|
}
|
|
expiresAt := p.Status.LastSeen.Add(expiresIn)
|
|
now := time.Now()
|
|
timeLeft := expiresAt.Sub(now)
|
|
return timeLeft <= 0, timeLeft
|
|
}
|
|
|
|
// LoginExpired indicates whether the peer's login has expired or not.
|
|
// If Peer.LastLogin plus the expiresIn duration has happened already; then login has expired.
|
|
// Return true if a login has expired, false otherwise, and time left to expiration (negative when expired).
|
|
// Login expiration can be disabled/enabled on a Peer level via Peer.LoginExpirationEnabled property.
|
|
// Login expiration can also be disabled/enabled globally on the Account level via Settings.PeerLoginExpirationEnabled.
|
|
// Only peers added by interactive SSO login can be expired.
|
|
func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) {
|
|
if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
|
|
return false, 0
|
|
}
|
|
expiresAt := p.GetLastLogin().Add(expiresIn)
|
|
now := time.Now()
|
|
timeLeft := expiresAt.Sub(now)
|
|
return timeLeft <= 0, timeLeft
|
|
}
|
|
|
|
// SessionExpiresAt returns the absolute UTC instant at which the peer's SSO
|
|
// session expires, derived from LastLogin and the account-level
|
|
// PeerLoginExpiration setting. Returns the zero value when login expiration
|
|
// does not apply (peer not SSO-registered, peer-level toggle off, or account
|
|
// expiry disabled). Callers should treat the zero value as "no deadline".
|
|
func (p *Peer) SessionExpiresAt(accountExpirationEnabled bool, expiresIn time.Duration) time.Time {
|
|
if !accountExpirationEnabled || !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled {
|
|
return time.Time{}
|
|
}
|
|
last := p.GetLastLogin()
|
|
if last.IsZero() {
|
|
return time.Time{}
|
|
}
|
|
return last.Add(expiresIn).UTC()
|
|
}
|
|
|
|
// FQDN returns peers FQDN combined of the peer's DNS label and the system's DNS domain
|
|
func (p *Peer) FQDN(dnsDomain string) string {
|
|
if dnsDomain == "" {
|
|
return ""
|
|
}
|
|
return p.DNSLabel + "." + dnsDomain
|
|
}
|
|
|
|
// EventMeta returns activity event meta related to the peer
|
|
func (p *Peer) EventMeta(dnsDomain string) map[string]any {
|
|
meta := map[string]any{"name": p.Name, "fqdn": p.FQDN(dnsDomain), "ip": p.IP, "created_at": p.CreatedAt,
|
|
"location_city_name": p.Location.CityName, "location_country_code": p.Location.CountryCode,
|
|
"location_geo_name_id": p.Location.GeoNameID, "location_connection_ip": p.Location.ConnectionIP}
|
|
if p.IPv6.IsValid() {
|
|
meta["ipv6"] = p.IPv6.String()
|
|
}
|
|
return meta
|
|
}
|
|
|
|
// Copy PeerStatus. SessionStartedAt must be propagated so clone-based
|
|
// callers (Peer.Copy, MarkLoginExpired, UpdateLastLogin) don't silently
|
|
// reset the fencing token to zero — that would let any subsequent
|
|
// SavePeerStatus write reopen the optimistic-lock window.
|
|
func (p *PeerStatus) Copy() *PeerStatus {
|
|
return &PeerStatus{
|
|
LastSeen: p.LastSeen,
|
|
SessionStartedAt: p.SessionStartedAt,
|
|
Connected: p.Connected,
|
|
LoginExpired: p.LoginExpired,
|
|
RequiresApproval: p.RequiresApproval,
|
|
}
|
|
}
|
|
|
|
// UpdateLastLogin and set login expired false
|
|
func (p *Peer) UpdateLastLogin() *Peer {
|
|
p.LastLogin = util.ToPtr(time.Now().UTC())
|
|
newStatus := p.Status.Copy()
|
|
newStatus.LoginExpired = false
|
|
p.Status = newStatus
|
|
return p
|
|
}
|
|
|
|
func (p *Peer) FromAPITemporaryAccessRequest(a *api.PeerTemporaryAccessRequest) {
|
|
p.Ephemeral = true
|
|
p.Name = a.Name
|
|
p.Key = a.WgPubKey
|
|
p.Meta = PeerSystemMeta{
|
|
Hostname: a.Name,
|
|
GoOS: "js",
|
|
OS: "js",
|
|
KernelVersion: "wasm",
|
|
}
|
|
}
|
|
|
|
func (f Flags) isEqual(other Flags) bool {
|
|
return f.RosenpassEnabled == other.RosenpassEnabled &&
|
|
f.RosenpassPermissive == other.RosenpassPermissive &&
|
|
f.ServerSSHAllowed == other.ServerSSHAllowed &&
|
|
f.RemoteJobsAllowed == other.RemoteJobsAllowed &&
|
|
f.DisableClientRoutes == other.DisableClientRoutes &&
|
|
f.DisableServerRoutes == other.DisableServerRoutes &&
|
|
f.DisableDNS == other.DisableDNS &&
|
|
f.DisableFirewall == other.DisableFirewall &&
|
|
f.BlockLANAccess == other.BlockLANAccess &&
|
|
f.BlockInbound == other.BlockInbound &&
|
|
f.LazyConnectionEnabled == other.LazyConnectionEnabled &&
|
|
f.DisableIPv6 == other.DisableIPv6
|
|
}
|