safing-portmaster/spn/hub/status.go

308 lines
6.8 KiB
Go

package hub
import (
"errors"
"fmt"
"sort"
"time"
"golang.org/x/exp/slices"
"github.com/safing/jess"
)
// VersionOffline is a special version used to signify that the Hub has gone offline.
// This is depracated, please use FlagOffline instead.
const VersionOffline = "offline"
// Status Flags.
const (
// FlagNetError signifies that the Hub reports a network connectivity failure or impairment.
FlagNetError = "net-error"
// FlagOffline signifies that the Hub has gone offline by itself.
FlagOffline = "offline"
// FlagAllowUnencrypted signifies that the Hub is available to handle unencrypted connections.
FlagAllowUnencrypted = "allow-unencrypted"
)
// Status is the message type used to update changing Hub Information. Changes are made automatically.
type Status struct {
Timestamp int64 `cbor:"t"`
// Version holds the current software version of the Hub.
Version string `cbor:"v"`
// Routing Information
Keys map[string]*Key `cbor:"k,omitempty" json:",omitempty"` // public keys (with type)
Lanes []*Lane `cbor:"c,omitempty" json:",omitempty"` // Connections to other Hubs.
// Status Information
// Load describes max(CPU, Memory) in percent, averaged over at least 15
// minutes. Load is published in fixed steps only.
Load int `cbor:"l,omitempty" json:",omitempty"`
// Flags holds flags that signify special states.
Flags []string `cbor:"f,omitempty" json:",omitempty"`
}
// Key represents a semi-ephemeral public key used for 0-RTT connection establishment.
type Key struct {
Scheme string
Key []byte
Expires int64
}
// Lane represents a connection to another Hub.
type Lane struct {
// ID is the Hub ID of the peer.
ID string
// Capacity designates the available bandwidth between these Hubs.
// It is specified in bit/s.
Capacity int
// Lateny designates the latency between these Hubs.
// It is specified in nanoseconds.
Latency time.Duration
}
// Copy returns a deep copy of the Status.
func (s *Status) Copy() *Status {
newStatus := &Status{
Timestamp: s.Timestamp,
Version: s.Version,
Lanes: slices.Clone(s.Lanes),
Load: s.Load,
Flags: slices.Clone(s.Flags),
}
// Copy map.
newStatus.Keys = make(map[string]*Key, len(s.Keys))
for k, v := range s.Keys {
newStatus.Keys[k] = v
}
return newStatus
}
// SelectSignet selects the public key to use for initiating connections to that Hub.
func (h *Hub) SelectSignet() *jess.Signet {
h.Lock()
defer h.Unlock()
// Return no Signet if we don't have a Status.
if h.Status == nil {
return nil
}
// TODO: select key based on preferred alg?
now := time.Now().Unix()
for id, key := range h.Status.Keys {
if now < key.Expires {
return &jess.Signet{
ID: id,
Scheme: key.Scheme,
Key: key.Key,
Public: true,
}
}
}
return nil
}
// GetSignet returns the public key identified by the given ID from the Hub Status.
func (h *Hub) GetSignet(id string, recipient bool) (*jess.Signet, error) {
h.Lock()
defer h.Unlock()
// check if public key is being requested
if !recipient {
return nil, jess.ErrSignetNotFound
}
// check if ID exists
key, ok := h.Status.Keys[id]
if !ok {
return nil, jess.ErrSignetNotFound
}
// transform and return
return &jess.Signet{
ID: id,
Scheme: key.Scheme,
Key: key.Key,
Public: true,
}, nil
}
// AddLane adds a new Lane to the Hub Status.
func (h *Hub) AddLane(newLane *Lane) error {
h.Lock()
defer h.Unlock()
// validity check
if h.Status == nil {
return ErrMissingInfo
}
// check if duplicate
for _, lane := range h.Status.Lanes {
if newLane.ID == lane.ID {
return errors.New("lane already exists")
}
}
// add
h.Status.Lanes = append(h.Status.Lanes, newLane)
return nil
}
// RemoveLane removes a Lane from the Hub Status.
func (h *Hub) RemoveLane(hubID string) error {
h.Lock()
defer h.Unlock()
// validity check
if h.Status == nil {
return ErrMissingInfo
}
for key, lane := range h.Status.Lanes {
if lane.ID == hubID {
h.Status.Lanes = append(h.Status.Lanes[:key], h.Status.Lanes[key+1:]...)
break
}
}
return nil
}
// GetLaneTo returns the lane to the given Hub, if it exists.
func (h *Hub) GetLaneTo(hubID string) *Lane {
h.Lock()
defer h.Unlock()
// validity check
if h.Status == nil {
return nil
}
for _, lane := range h.Status.Lanes {
if lane.ID == hubID {
return lane
}
}
return nil
}
// Equal returns whether the Lane is equal to the given one.
func (l *Lane) Equal(other *Lane) bool {
switch {
case l == nil || other == nil:
return false
case l.ID != other.ID:
return false
case l.Capacity != other.Capacity:
return false
case l.Latency != other.Latency:
return false
}
return true
}
// validateFormatting check if all values conform to the basic format.
func (s *Status) validateFormatting() error {
// public keys
if len(s.Keys) > 255 {
return fmt.Errorf("field Keys with array/slice length of %d exceeds max length of %d", len(s.Keys), 255)
}
for keyID, key := range s.Keys {
if err := checkStringFormat("Keys#ID", keyID, 255); err != nil {
return err
}
if err := checkStringFormat("Keys.Scheme", key.Scheme, 255); err != nil {
return err
}
if err := checkByteSliceFormat("Keys.Key", key.Key, 1024); err != nil {
return err
}
}
// connections
if len(s.Lanes) > 255 {
return fmt.Errorf("field Lanes with array/slice length of %d exceeds max length of %d", len(s.Lanes), 255)
}
for _, lanes := range s.Lanes {
if err := checkStringFormat("Lanes.ID", lanes.ID, 255); err != nil {
return err
}
}
// Flags
if err := checkStringSliceFormat("Flags", s.Flags, 255, 255); err != nil {
return err
}
return nil
}
func (l *Lane) String() string {
return fmt.Sprintf("<%s cap=%d lat=%d>", l.ID, l.Capacity, l.Latency)
}
// LanesEqual returns whether the given []*Lane are equal.
func LanesEqual(a, b []*Lane) bool {
if len(a) != len(b) {
return false
}
for i, l := range a {
if !l.Equal(b[i]) {
return false
}
}
return true
}
type lanes []*Lane
func (l lanes) Len() int { return len(l) }
func (l lanes) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l lanes) Less(i, j int) bool { return l[i].ID < l[j].ID }
// SortLanes sorts a slice of Lanes.
func SortLanes(l []*Lane) {
sort.Sort(lanes(l))
}
// HasFlag returns whether the Status has the given flag set.
func (s *Status) HasFlag(flagName string) bool {
return slices.Contains[[]string, string](s.Flags, flagName)
}
// FlagsEqual returns whether the given status flags are equal.
func FlagsEqual(a, b []string) bool {
// Cannot be equal if lengths are different.
if len(a) != len(b) {
return false
}
// If both are empty, they are equal.
if len(a) == 0 {
return true
}
// Make sure flags are sorted before comparing values.
sort.Strings(a)
sort.Strings(b)
// Compare values.
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}