Files
UNITRONIX 43e0b6f40a Integrate P2P/relay controls for issue #157.
Expose global connection strategy in the web panel with systemd/Docker persistence, extend server health diagnostics, enforce org network policy in the signal handler, and document when relay fallback is expected vs misconfiguration.
2026-06-04 00:26:58 +02:00

145 lines
3.4 KiB
Go

// Package policy resolves organization network policies for signal routing.
package policy
import (
"encoding/json"
"strings"
"sync"
"time"
"github.com/unitronix/betterdesk-server/db"
)
const networkPolicyKey = "policy_network"
// NetworkSettings is stored JSON under org_settings policy_network.
type NetworkSettings struct {
BlockDirectP2P bool `json:"block_direct_p2p"`
AllowedRelayServers []string `json:"allowed_relay_servers"`
IPAllowlist []string `json:"ip_allowlist"`
}
type cachedEntry struct {
settings NetworkSettings
expires time.Time
}
// NetworkResolver loads and caches org network policy for signal decisions.
type NetworkResolver struct {
db db.Database
mu sync.Mutex
org map[string]cachedEntry
dev map[string]cachedEntry
ttl time.Duration
}
// NewNetworkResolver creates a resolver with a 60s TTL cache.
func NewNetworkResolver(database db.Database) *NetworkResolver {
return &NetworkResolver{
db: database,
org: make(map[string]cachedEntry),
dev: make(map[string]cachedEntry),
ttl: 60 * time.Second,
}
}
func (r *NetworkResolver) settingsForOrg(orgID string) NetworkSettings {
if orgID == "" {
return NetworkSettings{}
}
r.mu.Lock()
defer r.mu.Unlock()
if ce, ok := r.org[orgID]; ok && time.Now().Before(ce.expires) {
return ce.settings
}
settings := NetworkSettings{}
raw, err := r.db.GetOrgSetting(orgID, networkPolicyKey)
if err == nil && raw != "" {
_ = json.Unmarshal([]byte(raw), &settings)
}
r.org[orgID] = cachedEntry{settings: settings, expires: time.Now().Add(r.ttl)}
return settings
}
// SettingsForDevice returns effective network policy for a registered device.
func (r *NetworkResolver) SettingsForDevice(deviceID string) NetworkSettings {
if deviceID == "" {
return NetworkSettings{}
}
r.mu.Lock()
if ce, ok := r.dev[deviceID]; ok && time.Now().Before(ce.expires) {
s := ce.settings
r.mu.Unlock()
return s
}
r.mu.Unlock()
orgID, _ := r.db.GetDeviceOrgID(deviceID)
settings := r.settingsForOrg(orgID)
r.mu.Lock()
r.dev[deviceID] = cachedEntry{settings: settings, expires: time.Now().Add(r.ttl)}
r.mu.Unlock()
return settings
}
// ShouldForceRelay returns true when any listed device belongs to an org with block_direct_p2p.
func (r *NetworkResolver) ShouldForceRelay(deviceIDs ...string) bool {
for _, id := range deviceIDs {
if id == "" {
continue
}
if r.SettingsForDevice(id).BlockDirectP2P {
return true
}
}
return false
}
// ResolveRelay picks a relay address allowed by org policy when an allowlist is configured.
func (r *NetworkResolver) ResolveRelay(defaultRelay string, deviceIDs ...string) string {
var allowed []string
for _, id := range deviceIDs {
if id == "" {
continue
}
s := r.SettingsForDevice(id)
if len(s.AllowedRelayServers) > 0 {
allowed = s.AllowedRelayServers
break
}
}
if len(allowed) == 0 {
return defaultRelay
}
normalizedDefault := strings.TrimSpace(defaultRelay)
for _, entry := range allowed {
entry = strings.TrimSpace(entry)
if entry == "" {
continue
}
if entry == normalizedDefault {
return defaultRelay
}
if normalizedDefault != "" && strings.HasPrefix(normalizedDefault, entry) {
return defaultRelay
}
}
return strings.TrimSpace(allowed[0])
}
// InvalidateOrg drops cached org/device entries after policy updates.
func (r *NetworkResolver) InvalidateOrg(orgID string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.org, orgID)
for devID, _ := range r.dev {
delete(r.dev, devID)
}
}