wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
202
spn/hub/database.go
Normal file
202
spn/hub/database.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/iterator"
|
||||
"github.com/safing/portbase/database/query"
|
||||
"github.com/safing/portbase/database/record"
|
||||
)
|
||||
|
||||
var (
|
||||
db = database.NewInterface(&database.Options{
|
||||
Local: true,
|
||||
Internal: true,
|
||||
})
|
||||
|
||||
getFromNavigator func(mapName, hubID string) *Hub
|
||||
)
|
||||
|
||||
// MakeHubDBKey makes a hub db key.
|
||||
func MakeHubDBKey(mapName, hubID string) string {
|
||||
return fmt.Sprintf("cache:spn/hubs/%s/%s", mapName, hubID)
|
||||
}
|
||||
|
||||
// MakeHubMsgDBKey makes a hub msg db key.
|
||||
func MakeHubMsgDBKey(mapName string, msgType MsgType, hubID string) string {
|
||||
return fmt.Sprintf("cache:spn/msgs/%s/%s/%s", mapName, msgType, hubID)
|
||||
}
|
||||
|
||||
// SetNavigatorAccess sets a shortcut function to access hubs from the navigator instead of having go through the database.
|
||||
// This also reduces the number of object in RAM and better caches parsed attributes.
|
||||
func SetNavigatorAccess(fn func(mapName, hubID string) *Hub) {
|
||||
if getFromNavigator == nil {
|
||||
getFromNavigator = fn
|
||||
}
|
||||
}
|
||||
|
||||
// GetHub get a Hub from the database - or the navigator, if configured.
|
||||
func GetHub(mapName string, hubID string) (*Hub, error) {
|
||||
if getFromNavigator != nil {
|
||||
hub := getFromNavigator(mapName, hubID)
|
||||
if hub != nil {
|
||||
return hub, nil
|
||||
}
|
||||
}
|
||||
|
||||
return GetHubByKey(MakeHubDBKey(mapName, hubID))
|
||||
}
|
||||
|
||||
// GetHubByKey returns a hub by its raw DB key.
|
||||
func GetHubByKey(key string) (*Hub, error) {
|
||||
r, err := db.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hub, err := EnsureHub(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return hub, nil
|
||||
}
|
||||
|
||||
// EnsureHub makes sure a database record is a Hub.
|
||||
func EnsureHub(r record.Record) (*Hub, error) {
|
||||
// unwrap
|
||||
if r.IsWrapped() {
|
||||
// only allocate a new struct, if we need it
|
||||
newHub := &Hub{}
|
||||
err := record.Unwrap(r, newHub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newHub = prepHub(newHub)
|
||||
|
||||
// Fully validate when getting from database.
|
||||
if err := newHub.Info.validateFormatting(); err != nil {
|
||||
return nil, fmt.Errorf("announcement failed format validation: %w", err)
|
||||
}
|
||||
if err := newHub.Status.validateFormatting(); err != nil {
|
||||
return nil, fmt.Errorf("status failed format validation: %w", err)
|
||||
}
|
||||
if err := newHub.Info.prepare(false); err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare announcement: %w", err)
|
||||
}
|
||||
|
||||
return newHub, nil
|
||||
}
|
||||
|
||||
// or adjust type
|
||||
newHub, ok := r.(*Hub)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("record not of type *Hub, but %T", r)
|
||||
}
|
||||
newHub = prepHub(newHub)
|
||||
|
||||
// Prepare only when already parsed.
|
||||
if err := newHub.Info.prepare(false); err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare announcement: %w", err)
|
||||
}
|
||||
|
||||
// ensure status
|
||||
return newHub, nil
|
||||
}
|
||||
|
||||
func prepHub(h *Hub) *Hub {
|
||||
if h.Status == nil {
|
||||
h.Status = &Status{}
|
||||
}
|
||||
h.Measurements = getSharedMeasurements(h.ID, h.Measurements)
|
||||
return h
|
||||
}
|
||||
|
||||
// Save saves to Hub to the correct scope in the database.
|
||||
func (h *Hub) Save() error {
|
||||
if !h.KeyIsSet() {
|
||||
h.SetKey(MakeHubDBKey(h.Map, h.ID))
|
||||
}
|
||||
|
||||
return db.Put(h)
|
||||
}
|
||||
|
||||
// RemoveHubAndMsgs deletes a Hub and it's saved messages from the database.
|
||||
func RemoveHubAndMsgs(mapName string, hubID string) (err error) {
|
||||
err = db.Delete(MakeHubDBKey(mapName, hubID))
|
||||
if err != nil && !errors.Is(err, database.ErrNotFound) {
|
||||
return fmt.Errorf("failed to delete main hub entry: %w", err)
|
||||
}
|
||||
|
||||
err = db.Delete(MakeHubMsgDBKey(mapName, MsgTypeAnnouncement, hubID))
|
||||
if err != nil && !errors.Is(err, database.ErrNotFound) {
|
||||
return fmt.Errorf("failed to delete hub announcement data: %w", err)
|
||||
}
|
||||
|
||||
err = db.Delete(MakeHubMsgDBKey(mapName, MsgTypeStatus, hubID))
|
||||
if err != nil && !errors.Is(err, database.ErrNotFound) {
|
||||
return fmt.Errorf("failed to delete hub status data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HubMsg stores raw Hub messages.
|
||||
type HubMsg struct { //nolint:golint
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
ID string
|
||||
Map string
|
||||
Type MsgType
|
||||
Data []byte
|
||||
|
||||
Received int64
|
||||
}
|
||||
|
||||
// SaveHubMsg saves a raw (and signed) message received by another Hub.
|
||||
func SaveHubMsg(id string, mapName string, msgType MsgType, data []byte) error {
|
||||
// create wrapper record
|
||||
msg := &HubMsg{
|
||||
ID: id,
|
||||
Map: mapName,
|
||||
Type: msgType,
|
||||
Data: data,
|
||||
Received: time.Now().Unix(),
|
||||
}
|
||||
// set key
|
||||
msg.SetKey(MakeHubMsgDBKey(msg.Map, msg.Type, msg.ID))
|
||||
// save
|
||||
return db.PutNew(msg)
|
||||
}
|
||||
|
||||
// QueryRawGossipMsgs queries the database for raw gossip messages.
|
||||
func QueryRawGossipMsgs(mapName string, msgType MsgType) (it *iterator.Iterator, err error) {
|
||||
it, err = db.Query(query.New(MakeHubMsgDBKey(mapName, msgType, "")))
|
||||
return
|
||||
}
|
||||
|
||||
// EnsureHubMsg makes sure a database record is a HubMsg.
|
||||
func EnsureHubMsg(r record.Record) (*HubMsg, error) {
|
||||
// unwrap
|
||||
if r.IsWrapped() {
|
||||
// only allocate a new struct, if we need it
|
||||
newHubMsg := &HubMsg{}
|
||||
err := record.Unwrap(r, newHubMsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newHubMsg, nil
|
||||
}
|
||||
|
||||
// or adjust type
|
||||
newHubMsg, ok := r.(*HubMsg)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("record not of type *Hub, but %T", r)
|
||||
}
|
||||
return newHubMsg, nil
|
||||
}
|
||||
21
spn/hub/errors.go
Normal file
21
spn/hub/errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package hub
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrMissingInfo signifies that the hub is missing the HubAnnouncement.
|
||||
ErrMissingInfo = errors.New("hub has no announcement")
|
||||
|
||||
// ErrMissingTransports signifies that the hub announcement did not specify any transports.
|
||||
ErrMissingTransports = errors.New("hub announcement has no transports")
|
||||
|
||||
// ErrMissingIPs signifies that the hub announcement did not specify any IPs,
|
||||
// or none of the IPs is supported by the client.
|
||||
ErrMissingIPs = errors.New("hub announcement has no (supported) IPs")
|
||||
|
||||
// ErrTemporaryValidationError is returned when a validation error might be temporary.
|
||||
ErrTemporaryValidationError = errors.New("temporary validation error")
|
||||
|
||||
// ErrOldData is returned when received data is outdated.
|
||||
ErrOldData = errors.New("")
|
||||
)
|
||||
69
spn/hub/format.go
Normal file
69
spn/hub/format.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
// BaselineCharset defines the permitted characters.
|
||||
var BaselineCharset = regexp.MustCompile(
|
||||
// Start of charset selection.
|
||||
`^[` +
|
||||
// Printable ASCII (character code 32-127), excluding common control characters of different languages: "$%&';<>\` and DELETE.
|
||||
` !#()*+,\-\./0-9:=?@A-Z[\]^_a-z{|}~` +
|
||||
// Only latin characters from extended ASCII (character code 128-255).
|
||||
`ŠŒŽšœžŸ¡¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ` +
|
||||
// End of charset selection.
|
||||
`]*$`,
|
||||
)
|
||||
|
||||
func checkStringFormat(fieldName, value string, maxLength int) error {
|
||||
switch {
|
||||
case len(value) > maxLength:
|
||||
return fmt.Errorf("field %s with length of %d exceeds max length of %d", fieldName, len(value), maxLength)
|
||||
case !BaselineCharset.MatchString(value):
|
||||
return fmt.Errorf("field %s contains characters not permitted by baseline validation", fieldName)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkStringSliceFormat(fieldName string, value []string, maxLength, maxStringLength int) error { //nolint:unparam
|
||||
if len(value) > maxLength {
|
||||
return fmt.Errorf("field %s with array/slice length of %d exceeds max length of %d", fieldName, len(value), maxLength)
|
||||
}
|
||||
for _, s := range value {
|
||||
if err := checkStringFormat(fieldName, s, maxStringLength); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkByteSliceFormat(fieldName string, value []byte, maxLength int) error {
|
||||
switch {
|
||||
case len(value) > maxLength:
|
||||
return fmt.Errorf("field %s with length of %d exceeds max length of %d", fieldName, len(value), maxLength)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkIPFormat(fieldName string, value net.IP) error {
|
||||
// Check if there is an IP address.
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(value) != 4 && len(value) != 16:
|
||||
return fmt.Errorf("field %s has an invalid length of %d for an IP address", fieldName, len(value))
|
||||
case netutils.GetIPScope(value) == netutils.Invalid:
|
||||
return fmt.Errorf("field %s holds an invalid IP address: %s", fieldName, value)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
81
spn/hub/format_test.go
Normal file
81
spn/hub/format_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckStringFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testSet := map[string]bool{
|
||||
// Printable ASCII (character code 32-127)
|
||||
" ": true, "!": true, `"`: false, "#": true, "$": false, "%": false, "&": false, "'": false,
|
||||
"(": true, ")": true, "*": true, "+": true, ",": true, "-": true, ".": true, "/": true,
|
||||
"0": true, "1": true, "2": true, "3": true, "4": true, "5": true, "6": true, "7": true,
|
||||
"8": true, "9": true, ":": true, ";": false, "<": false, "=": true, ">": false, "?": true,
|
||||
"@": true, "A": true, "B": true, "C": true, "D": true, "E": true, "F": true, "G": true,
|
||||
"H": true, "I": true, "J": true, "K": true, "L": true, "M": true, "N": true, "O": true,
|
||||
"P": true, "Q": true, "R": true, "S": true, "T": true, "U": true, "V": true, "W": true,
|
||||
"X": true, "Y": true, "Z": true, "[": true, `\`: false, "]": true, "^": true, "_": true,
|
||||
"`": false, "a": true, "b": true, "c": true, "d": true, "e": true, "f": true, "g": true,
|
||||
"h": true, "i": true, "j": true, "k": true, "l": true, "m": true, "n": true, "o": true,
|
||||
"p": true, "q": true, "r": true, "s": true, "t": true, "u": true, "v": true, "w": true,
|
||||
"x": true, "y": true, "z": true, "{": true, "|": true, "}": true, "~": true,
|
||||
// Not testing for DELETE character.
|
||||
|
||||
// Extended ASCII (character code 128-255)
|
||||
"€": false, "‚": false, "ƒ": false, "„": false, "…": false, "†": false, "‡": false, "ˆ": false,
|
||||
"‰": false, "Š": true, "‹": false, "Œ": true, "Ž": true, "‘": false, "’": false, "“": false,
|
||||
"”": false, "•": false, "–": false, "—": false, "˜": false, "™": false, "š": true, "›": false,
|
||||
"œ": true, "ž": true, "Ÿ": true, "¡": true, "¢": false, "£": false, "¤": false, "¥": false,
|
||||
"¦": false, "§": false, "¨": false, "©": false, "ª": false, "«": false, "¬": false, "®": false,
|
||||
"¯": false, "°": false, "±": false, "²": false, "³": false, "´": false, "µ": false, "¶": false,
|
||||
"·": false, "¸": false, "¹": false, "º": false, "»": false, "¼": false, "½": false, "¾": false,
|
||||
"¿": true, "À": true, "Á": true, "Â": true, "Ã": true, "Ä": true, "Å": true, "Æ": true,
|
||||
"Ç": true, "È": true, "É": true, "Ê": true, "Ë": true, "Ì": true, "Í": true, "Î": true,
|
||||
"Ï": true, "Ð": true, "Ñ": true, "Ò": true, "Ó": true, "Ô": true, "Õ": true, "Ö": true,
|
||||
"×": false, "Ø": true, "Ù": true, "Ú": true, "Û": true, "Ü": true, "Ý": true, "Þ": true,
|
||||
"ß": true, "à": true, "á": true, "â": true, "ã": true, "ä": true, "å": true, "æ": true,
|
||||
"ç": true, "è": true, "é": true, "ê": true, "ë": true, "ì": true, "í": true, "î": true,
|
||||
"ï": true, "ð": true, "ñ": true, "ò": true, "ó": true, "ô": true, "õ": true, "ö": true,
|
||||
"÷": false, "ø": true, "ù": true, "ú": true, "û": true, "ü": true, "ý": true, "þ": true,
|
||||
"ÿ": true,
|
||||
}
|
||||
|
||||
for testCharacter, isPermitted := range testSet {
|
||||
if isPermitted {
|
||||
assert.NoError(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3))
|
||||
} else {
|
||||
assert.Error(t, checkStringFormat(fmt.Sprintf("test character %q", testCharacter), testCharacter, 3))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckIPFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// IPv4
|
||||
assert.NoError(t, checkIPFormat("test IP 1.1.1.1", net.IPv4(1, 1, 1, 1)))
|
||||
assert.NoError(t, checkIPFormat("test IP 192.168.1.1", net.IPv4(192, 168, 1, 1)))
|
||||
assert.Error(t, checkIPFormat("test IP 255.0.0.1", net.IPv4(255, 0, 0, 1)))
|
||||
|
||||
// IPv6
|
||||
assert.NoError(t, checkIPFormat("test IP ::1", net.ParseIP("::1")))
|
||||
assert.NoError(t, checkIPFormat("test IP 2606:4700:4700::1111", net.ParseIP("2606:4700:4700::1111")))
|
||||
|
||||
// Invalid
|
||||
assert.Error(t, checkIPFormat("test IP with length 3", net.IP([]byte{0, 0, 0})))
|
||||
assert.Error(t, checkIPFormat("test IP with length 5", net.IP([]byte{0, 0, 0, 0, 0})))
|
||||
assert.Error(t, checkIPFormat(
|
||||
"test IP with length 15",
|
||||
net.IP([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
|
||||
))
|
||||
assert.Error(t, checkIPFormat(
|
||||
"test IP with length 17",
|
||||
net.IP([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
|
||||
))
|
||||
}
|
||||
435
spn/hub/hub.go
Normal file
435
spn/hub/hub.go
Normal file
@@ -0,0 +1,435 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
)
|
||||
|
||||
// Scope is the network scope a Hub can be in.
|
||||
type Scope uint8
|
||||
|
||||
const (
|
||||
// ScopeInvalid defines an invalid scope.
|
||||
ScopeInvalid Scope = 0
|
||||
|
||||
// ScopeLocal identifies local Hubs.
|
||||
ScopeLocal Scope = 1
|
||||
|
||||
// ScopePublic identifies public Hubs.
|
||||
ScopePublic Scope = 2
|
||||
|
||||
// ScopeTest identifies Hubs for testing.
|
||||
ScopeTest Scope = 0xFF
|
||||
)
|
||||
|
||||
const (
|
||||
obsoleteValidAfter = 30 * 24 * time.Hour
|
||||
obsoleteInvalidAfter = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// MsgType defines the message type.
|
||||
type MsgType string
|
||||
|
||||
// Message Types.
|
||||
const (
|
||||
MsgTypeAnnouncement = "announcement"
|
||||
MsgTypeStatus = "status"
|
||||
)
|
||||
|
||||
// Hub represents a network node in the SPN.
|
||||
type Hub struct { //nolint:maligned
|
||||
sync.Mutex
|
||||
record.Base
|
||||
|
||||
ID string
|
||||
PublicKey *jess.Signet
|
||||
Map string
|
||||
|
||||
Info *Announcement
|
||||
Status *Status
|
||||
|
||||
Measurements *Measurements
|
||||
measurementsInitialized bool
|
||||
|
||||
FirstSeen time.Time
|
||||
VerifiedIPs bool
|
||||
InvalidInfo bool
|
||||
InvalidStatus bool
|
||||
}
|
||||
|
||||
// Announcement is the main message type to publish Hub Information. This only changes if updated manually.
|
||||
type Announcement struct {
|
||||
// Primary Key
|
||||
// hash of public key
|
||||
// must be checked if it matches the public key
|
||||
ID string `cbor:"i"` // via jess.LabeledHash
|
||||
|
||||
// PublicKey *jess.Signet
|
||||
// PublicKey // if not part of signature
|
||||
// Signature *jess.Letter
|
||||
Timestamp int64 `cbor:"t"` // Unix timestamp in seconds
|
||||
|
||||
// Node Information
|
||||
Name string `cbor:"n"` // name of the node
|
||||
Group string `cbor:"g,omitempty" json:",omitempty"` // person or organisation, who is in control of the node (should be same for all nodes of this person or organisation)
|
||||
ContactAddress string `cbor:"ca,omitempty" json:",omitempty"` // contact possibility (recommended, but optional)
|
||||
ContactService string `cbor:"cs,omitempty" json:",omitempty"` // type of service of the contact address, if not email
|
||||
|
||||
// currently unused, but collected for later use
|
||||
Hosters []string `cbor:"ho,omitempty" json:",omitempty"` // hoster supply chain (reseller, hosting provider, datacenter operator, ...)
|
||||
Datacenter string `cbor:"dc,omitempty" json:",omitempty"` // datacenter will be bullshit checked
|
||||
// Format: CC-COMPANY-INTERNALCODE
|
||||
// Eg: DE-Hetzner-FSN1-DC5
|
||||
|
||||
// Network Location and Access
|
||||
// If node is behind NAT (or similar), IP addresses must be configured
|
||||
IPv4 net.IP `cbor:"ip4,omitempty" json:",omitempty"` // must be global and accessible
|
||||
IPv6 net.IP `cbor:"ip6,omitempty" json:",omitempty"` // must be global and accessible
|
||||
Transports []string `cbor:"tp,omitempty" json:",omitempty"`
|
||||
// {
|
||||
// "spn:17",
|
||||
// "smtp:25", // also support "smtp://:25
|
||||
// "smtp:587",
|
||||
// "imap:143",
|
||||
// "http:80",
|
||||
// "http://example.com:80", // HTTP (based): use full path for request
|
||||
// "https:443",
|
||||
// "ws:80",
|
||||
// "wss://example.com:443/spn",
|
||||
// } // protocols with metadata
|
||||
parsedTransports []*Transport
|
||||
|
||||
// Policies - default permit
|
||||
Entry []string `cbor:"pi,omitempty" json:",omitempty"`
|
||||
entryPolicy endpoints.Endpoints
|
||||
// {"+ ", "- *"}
|
||||
Exit []string `cbor:"po,omitempty" json:",omitempty"`
|
||||
exitPolicy endpoints.Endpoints
|
||||
// {"- * TCP/25", "- US"}
|
||||
|
||||
// Flags holds flags that signify special states.
|
||||
Flags []string `cbor:"f,omitempty" json:",omitempty"`
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the Announcement.
|
||||
func (a *Announcement) Copy() *Announcement {
|
||||
return &Announcement{
|
||||
ID: a.ID,
|
||||
Timestamp: a.Timestamp,
|
||||
Name: a.Name,
|
||||
ContactAddress: a.ContactAddress,
|
||||
ContactService: a.ContactService,
|
||||
Hosters: slices.Clone(a.Hosters),
|
||||
Datacenter: a.Datacenter,
|
||||
IPv4: a.IPv4,
|
||||
IPv6: a.IPv6,
|
||||
Transports: slices.Clone(a.Transports),
|
||||
parsedTransports: slices.Clone(a.parsedTransports),
|
||||
Entry: slices.Clone(a.Entry),
|
||||
entryPolicy: slices.Clone(a.entryPolicy),
|
||||
Exit: slices.Clone(a.Exit),
|
||||
exitPolicy: slices.Clone(a.exitPolicy),
|
||||
Flags: slices.Clone(a.Flags),
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo returns the hub info.
|
||||
func (h *Hub) GetInfo() *Announcement {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
return h.Info
|
||||
}
|
||||
|
||||
// GetStatus returns the hub status.
|
||||
func (h *Hub) GetStatus() *Status {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
return h.Status
|
||||
}
|
||||
|
||||
// GetMeasurements returns the hub measurements.
|
||||
// This method should always be used instead of direct access.
|
||||
func (h *Hub) GetMeasurements() *Measurements {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
return h.GetMeasurementsWithLockedHub()
|
||||
}
|
||||
|
||||
// GetMeasurementsWithLockedHub returns the hub measurements.
|
||||
// The caller must hold the lock to Hub.
|
||||
// This method should always be used instead of direct access.
|
||||
func (h *Hub) GetMeasurementsWithLockedHub() *Measurements {
|
||||
if !h.measurementsInitialized {
|
||||
h.Measurements = getSharedMeasurements(h.ID, h.Measurements)
|
||||
h.Measurements.check()
|
||||
h.measurementsInitialized = true
|
||||
}
|
||||
|
||||
return h.Measurements
|
||||
}
|
||||
|
||||
// Verified return whether the Hub has been verified.
|
||||
func (h *Hub) Verified() bool {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
return h.VerifiedIPs
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the Hub.
|
||||
func (h *Hub) String() string {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
return "<Hub " + h.getName() + ">"
|
||||
}
|
||||
|
||||
// StringWithoutLocking returns a human-readable representation of the Hub without locking it.
|
||||
func (h *Hub) StringWithoutLocking() string {
|
||||
return "<Hub " + h.getName() + ">"
|
||||
}
|
||||
|
||||
// Name returns a human-readable version of a Hub's name. This name will likely consist of two parts: the given name and the ending of the ID to make it unique.
|
||||
func (h *Hub) Name() string {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
return h.getName()
|
||||
}
|
||||
|
||||
func (h *Hub) getName() string {
|
||||
// Check for a short ID that is sometimes used for testing.
|
||||
if len(h.ID) < 8 {
|
||||
return h.ID
|
||||
}
|
||||
|
||||
shortenedID := h.ID[len(h.ID)-8:len(h.ID)-4] +
|
||||
"-" +
|
||||
h.ID[len(h.ID)-4:]
|
||||
|
||||
// Be more careful, as the Hub name is user input.
|
||||
switch {
|
||||
case h.Info.Name == "":
|
||||
return shortenedID
|
||||
case len(h.Info.Name) > 16:
|
||||
return h.Info.Name[:16] + " " + shortenedID
|
||||
default:
|
||||
return h.Info.Name + " " + shortenedID
|
||||
}
|
||||
}
|
||||
|
||||
// Obsolete returns if the Hub is obsolete and may be deleted.
|
||||
func (h *Hub) Obsolete() bool {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
// Check if Hub is valid.
|
||||
var valid bool
|
||||
switch {
|
||||
case h.InvalidInfo:
|
||||
case h.InvalidStatus:
|
||||
case h.HasFlag(FlagOffline):
|
||||
// Treat offline as invalid.
|
||||
default:
|
||||
valid = true
|
||||
}
|
||||
|
||||
// Check when Hub was last seen.
|
||||
lastSeen := h.FirstSeen
|
||||
if h.Status.Timestamp != 0 {
|
||||
lastSeen = time.Unix(h.Status.Timestamp, 0)
|
||||
}
|
||||
|
||||
// Check if Hub is obsolete.
|
||||
if valid {
|
||||
return time.Now().Add(-obsoleteValidAfter).After(lastSeen)
|
||||
}
|
||||
return time.Now().Add(-obsoleteInvalidAfter).After(lastSeen)
|
||||
}
|
||||
|
||||
// HasFlag returns whether the Announcement or Status has the given flag set.
|
||||
func (h *Hub) HasFlag(flagName string) bool {
|
||||
switch {
|
||||
case h.Status != nil && slices.Contains[[]string, string](h.Status.Flags, flagName):
|
||||
return true
|
||||
case h.Info != nil && slices.Contains[[]string, string](h.Info.Flags, flagName):
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Equal returns whether the given Announcements are equal.
|
||||
func (a *Announcement) Equal(b *Announcement) bool {
|
||||
switch {
|
||||
case a == nil || b == nil:
|
||||
return false
|
||||
case a.ID != b.ID:
|
||||
return false
|
||||
case a.Timestamp != b.Timestamp:
|
||||
return false
|
||||
case a.Name != b.Name:
|
||||
return false
|
||||
case a.ContactAddress != b.ContactAddress:
|
||||
return false
|
||||
case a.ContactService != b.ContactService:
|
||||
return false
|
||||
case !equalStringSlice(a.Hosters, b.Hosters):
|
||||
return false
|
||||
case a.Datacenter != b.Datacenter:
|
||||
return false
|
||||
case !a.IPv4.Equal(b.IPv4):
|
||||
return false
|
||||
case !a.IPv6.Equal(b.IPv6):
|
||||
return false
|
||||
case !equalStringSlice(a.Transports, b.Transports):
|
||||
return false
|
||||
case !equalStringSlice(a.Entry, b.Entry):
|
||||
return false
|
||||
case !equalStringSlice(a.Exit, b.Exit):
|
||||
return false
|
||||
case !equalStringSlice(a.Flags, b.Flags):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// validateFormatting check if all values conform to the basic format.
|
||||
func (a *Announcement) validateFormatting() error {
|
||||
if err := checkStringFormat("ID", a.ID, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringFormat("Name", a.Name, 32); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringFormat("Group", a.Group, 32); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringFormat("ContactAddress", a.ContactAddress, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringFormat("ContactService", a.ContactService, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringSliceFormat("Hosters", a.Hosters, 255, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringFormat("Datacenter", a.Datacenter, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIPFormat("IPv4", a.IPv4); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkIPFormat("IPv6", a.IPv6); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringSliceFormat("Transports", a.Transports, 255, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringSliceFormat("Entry", a.Entry, 255, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringSliceFormat("Exit", a.Exit, 255, 255); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkStringSliceFormat("Flags", a.Flags, 16, 32); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepare prepares the announcement by parsing policies and transports.
|
||||
// If fields are already parsed, they will only be parsed again, when force is set to true.
|
||||
func (a *Announcement) prepare(force bool) error {
|
||||
var err error
|
||||
|
||||
// Parse policies.
|
||||
if len(a.entryPolicy) == 0 || force {
|
||||
if a.entryPolicy, err = endpoints.ParseEndpoints(a.Entry); err != nil {
|
||||
return fmt.Errorf("failed to parse entry policy: %w", err)
|
||||
}
|
||||
}
|
||||
if len(a.exitPolicy) == 0 || force {
|
||||
if a.exitPolicy, err = endpoints.ParseEndpoints(a.Exit); err != nil {
|
||||
return fmt.Errorf("failed to parse exit policy: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse transports.
|
||||
if len(a.parsedTransports) == 0 || force {
|
||||
parsed, errs := ParseTransports(a.Transports)
|
||||
// Log parsing warnings.
|
||||
for _, err := range errs {
|
||||
log.Warningf("hub: Hub %s (%s) has configured an %s", a.Name, a.ID, err)
|
||||
}
|
||||
// Check if there are any valid transports.
|
||||
if len(parsed) == 0 {
|
||||
return ErrMissingTransports
|
||||
}
|
||||
a.parsedTransports = parsed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EntryPolicy returns the Hub's entry policy.
|
||||
func (a *Announcement) EntryPolicy() endpoints.Endpoints {
|
||||
return a.entryPolicy
|
||||
}
|
||||
|
||||
// ExitPolicy returns the Hub's exit policy.
|
||||
func (a *Announcement) ExitPolicy() endpoints.Endpoints {
|
||||
return a.exitPolicy
|
||||
}
|
||||
|
||||
// ParsedTransports returns the Hub's parsed transports.
|
||||
func (a *Announcement) ParsedTransports() []*Transport {
|
||||
return a.parsedTransports
|
||||
}
|
||||
|
||||
// HasFlag returns whether the Announcement has the given flag set.
|
||||
func (a *Announcement) HasFlag(flagName string) bool {
|
||||
return slices.Contains[[]string, string](a.Flags, flagName)
|
||||
}
|
||||
|
||||
// String returns the string representation of the scope.
|
||||
func (s Scope) String() string {
|
||||
switch s {
|
||||
case ScopeInvalid:
|
||||
return "invalid"
|
||||
case ScopeLocal:
|
||||
return "local"
|
||||
case ScopePublic:
|
||||
return "public"
|
||||
case ScopeTest:
|
||||
return "test"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func equalStringSlice(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
79
spn/hub/hub_test.go
Normal file
79
spn/hub/hub_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
_ "github.com/safing/portmaster/service/core/base"
|
||||
"github.com/safing/portmaster/service/core/pmtesting"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// TODO: We need the database module, so maybe set up a module for this package.
|
||||
module := modules.Register("hub", nil, nil, nil, "base")
|
||||
pmtesting.TestMain(m, module)
|
||||
}
|
||||
|
||||
func TestEquality(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// empty match
|
||||
a := &Announcement{}
|
||||
assert.True(t, a.Equal(a), "should match itself") //nolint:gocritic // This is a test.
|
||||
|
||||
// full match
|
||||
a = &Announcement{
|
||||
ID: "a",
|
||||
Timestamp: 1,
|
||||
Name: "a",
|
||||
ContactAddress: "a",
|
||||
ContactService: "a",
|
||||
Hosters: []string{"a", "b"},
|
||||
Datacenter: "a",
|
||||
IPv4: net.IPv4(1, 2, 3, 4),
|
||||
IPv6: net.ParseIP("::1"),
|
||||
Transports: []string{"a", "b"},
|
||||
Entry: []string{"a", "b"},
|
||||
Exit: []string{"a", "b"},
|
||||
}
|
||||
assert.True(t, a.Equal(a), "should match itself") //nolint:gocritic // This is a test.
|
||||
|
||||
// no match
|
||||
b := &Announcement{ID: "b"}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{Timestamp: 2}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{Name: "b"}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{ContactAddress: "b"}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{ContactService: "b"}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{Hosters: []string{"b", "c"}}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{Datacenter: "b"}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{IPv4: net.IPv4(1, 2, 3, 5)}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{IPv6: net.ParseIP("::2")}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{Transports: []string{"b", "c"}}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{Entry: []string{"b", "c"}}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
b = &Announcement{Exit: []string{"b", "c"}}
|
||||
assert.False(t, a.Equal(b), "should not match")
|
||||
}
|
||||
|
||||
func TestStringify(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert.Equal(t, "<Hub abcdefg>", (&Hub{ID: "abcdefg", Info: &Announcement{}}).String())
|
||||
assert.Equal(t, "<Hub abcd-efgh>", (&Hub{ID: "abcdefgh", Info: &Announcement{}}).String())
|
||||
assert.Equal(t, "<Hub bcde-fghi>", (&Hub{ID: "abcdefghi", Info: &Announcement{}}).String())
|
||||
assert.Equal(t, "<Hub Franz bcde-fghi>", (&Hub{ID: "abcdefghi", Info: &Announcement{Name: "Franz"}}).String())
|
||||
assert.Equal(t, "<Hub AProbablyAutoGen bcde-fghi>", (&Hub{ID: "abcdefghi", Info: &Announcement{Name: "AProbablyAutoGeneratedName"}}).String())
|
||||
}
|
||||
191
spn/hub/intel.go
Normal file
191
spn/hub/intel.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/safing/jess/lhash"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
)
|
||||
|
||||
// Intel holds a collection of various security related data collections on Hubs.
|
||||
type Intel struct {
|
||||
// BootstrapHubs is list of transports that also contain an IP and the Hub's ID.
|
||||
BootstrapHubs []string
|
||||
|
||||
// Hubs holds intel regarding specific Hubs.
|
||||
Hubs map[string]*HubIntel
|
||||
|
||||
// AdviseOnlyTrustedHubs advises to only use trusted Hubs regardless of intended purpose.
|
||||
AdviseOnlyTrustedHubs bool
|
||||
// AdviseOnlyTrustedHomeHubs advises to only use trusted Hubs for Home Hubs.
|
||||
AdviseOnlyTrustedHomeHubs bool
|
||||
// AdviseOnlyTrustedDestinationHubs advises to only use trusted Hubs for Destination Hubs.
|
||||
AdviseOnlyTrustedDestinationHubs bool
|
||||
|
||||
// Hub Advisories advise on the usage of Hubs and take the form of Endpoint Lists that match on both IPv4 and IPv6 addresses and their related data.
|
||||
|
||||
// HubAdvisory always affects all Hubs.
|
||||
HubAdvisory []string
|
||||
// HomeHubAdvisory is only taken into account when selecting a Home Hub.
|
||||
HomeHubAdvisory []string
|
||||
// DestinationHubAdvisory is only taken into account when selecting a Destination Hub.
|
||||
DestinationHubAdvisory []string
|
||||
|
||||
// Regions defines regions to assist network optimization.
|
||||
Regions []*RegionConfig
|
||||
|
||||
// VirtualNetworks holds network configurations for virtual cloud networks.
|
||||
VirtualNetworks []*VirtualNetworkConfig
|
||||
|
||||
parsed *ParsedIntel
|
||||
}
|
||||
|
||||
// HubIntel holds Hub-related data.
|
||||
type HubIntel struct { //nolint:golint
|
||||
// Trusted specifies if the Hub is specially designated for more sensitive tasks, such as handling unencrypted traffic.
|
||||
Trusted bool
|
||||
|
||||
// Discontinued specifies if the Hub has been discontinued and should be marked as offline and removed.
|
||||
Discontinued bool
|
||||
|
||||
// VerifiedOwner holds the name of the verified owner / operator of the Hub.
|
||||
VerifiedOwner string
|
||||
|
||||
// Override is used to override certain Hub information.
|
||||
Override *InfoOverride
|
||||
}
|
||||
|
||||
// RegionConfig holds the configuration of a region.
|
||||
type RegionConfig struct {
|
||||
// ID is the internal identifier of the region.
|
||||
ID string
|
||||
// Name is a human readable name of the region.
|
||||
Name string
|
||||
// MemberPolicy specifies a list for including members.
|
||||
MemberPolicy []string
|
||||
|
||||
// RegionalMinLanes specifies how many lanes other regions should build
|
||||
// to this region.
|
||||
RegionalMinLanes int
|
||||
// RegionalMinLanesPerHub specifies how many lanes other regions should
|
||||
// build to this region, per Hub in this region.
|
||||
// This value will usually be below one.
|
||||
RegionalMinLanesPerHub float64
|
||||
// RegionalMaxLanesOnHub specifies how many lanes from or to another region may be
|
||||
// built on one Hub per region.
|
||||
RegionalMaxLanesOnHub int
|
||||
|
||||
// SatelliteMinLanes specifies how many lanes satellites (Hubs without
|
||||
// region) should build to this region.
|
||||
SatelliteMinLanes int
|
||||
// SatelliteMinLanesPerHub specifies how many lanes satellites (Hubs without
|
||||
// region) should build to this region, per Hub in this region.
|
||||
// This value will usually be below one.
|
||||
SatelliteMinLanesPerHub float64
|
||||
|
||||
// InternalMinLanesOnHub specifies how many lanes every Hub should create
|
||||
// within the region at minimum.
|
||||
InternalMinLanesOnHub int
|
||||
// InternalMaxHops specifies the max hop constraint for internally optimizing
|
||||
// the region.
|
||||
InternalMaxHops int
|
||||
}
|
||||
|
||||
// VirtualNetworkConfig holds configuration of a virtual network that binds multiple Hubs together.
|
||||
type VirtualNetworkConfig struct {
|
||||
// Name is a human readable name of the virtual network.
|
||||
Name string
|
||||
// Force forces the use of the mapped IP addresses after the Hub's IPs have been verified.
|
||||
Force bool
|
||||
// Mapping maps Hub IDs to internal IP addresses.
|
||||
Mapping map[string]net.IP
|
||||
}
|
||||
|
||||
// ParsedIntel holds a collection of parsed intel data.
|
||||
type ParsedIntel struct {
|
||||
// HubAdvisory always affects all Hubs.
|
||||
HubAdvisory endpoints.Endpoints
|
||||
|
||||
// HomeHubAdvisory is only taken into account when selecting a Home Hub.
|
||||
HomeHubAdvisory endpoints.Endpoints
|
||||
|
||||
// DestinationHubAdvisory is only taken into account when selecting a Destination Hub.
|
||||
DestinationHubAdvisory endpoints.Endpoints
|
||||
}
|
||||
|
||||
// Parsed returns the collection of parsed intel data.
|
||||
func (i *Intel) Parsed() *ParsedIntel {
|
||||
return i.parsed
|
||||
}
|
||||
|
||||
// ParseIntel parses Hub intelligence data.
|
||||
func ParseIntel(data []byte) (*Intel, error) {
|
||||
// Load data into struct.
|
||||
intel := &Intel{}
|
||||
err := yaml.Unmarshal(data, intel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse data: %w", err)
|
||||
}
|
||||
|
||||
// Parse all endpoint lists.
|
||||
err = intel.ParseAdvisories()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return intel, nil
|
||||
}
|
||||
|
||||
// ParseAdvisories parses all advisory endpoint lists.
|
||||
func (i *Intel) ParseAdvisories() (err error) {
|
||||
i.parsed = &ParsedIntel{}
|
||||
|
||||
i.parsed.HubAdvisory, err = endpoints.ParseEndpoints(i.HubAdvisory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse HubAdvisory list: %w", err)
|
||||
}
|
||||
|
||||
i.parsed.HomeHubAdvisory, err = endpoints.ParseEndpoints(i.HomeHubAdvisory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse HomeHubAdvisory list: %w", err)
|
||||
}
|
||||
|
||||
i.parsed.DestinationHubAdvisory, err = endpoints.ParseEndpoints(i.DestinationHubAdvisory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse DestinationHubAdvisory list: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseBootstrapHub parses a bootstrap hub.
|
||||
func ParseBootstrapHub(bootstrapTransport string) (t *Transport, hubID string, hubIP net.IP, err error) {
|
||||
// Parse transport and check Hub ID.
|
||||
t, err = ParseTransport(bootstrapTransport)
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to parse transport: %w", err)
|
||||
}
|
||||
if t.Option == "" {
|
||||
return nil, "", nil, errors.New("missing hub ID in URL fragment")
|
||||
}
|
||||
if _, err := lhash.FromBase58(t.Option); err != nil {
|
||||
return nil, "", nil, fmt.Errorf("hub ID is invalid: %w", err)
|
||||
}
|
||||
|
||||
// Parse IP address from transport.
|
||||
ip := net.ParseIP(t.Domain)
|
||||
if ip == nil {
|
||||
return nil, "", nil, errors.New("invalid IP address (domains are not supported for bootstrapping)")
|
||||
}
|
||||
|
||||
// Clean up transport for hub info.
|
||||
id := t.Option
|
||||
t.Domain = ""
|
||||
t.Option = ""
|
||||
|
||||
return t, id, ip, nil
|
||||
}
|
||||
17
spn/hub/intel_override.go
Normal file
17
spn/hub/intel_override.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package hub
|
||||
|
||||
import "github.com/safing/portmaster/service/intel/geoip"
|
||||
|
||||
// InfoOverride holds data to overide hub info information.
|
||||
type InfoOverride struct {
|
||||
// ContinentCode overrides the continent code of the geoip data.
|
||||
ContinentCode string
|
||||
// CountryCode overrides the country code of the geoip data.
|
||||
CountryCode string
|
||||
// Coordinates overrides the geo coordinates code of the geoip data.
|
||||
Coordinates *geoip.Coordinates
|
||||
// ASN overrides the Autonomous System Number of the geoip data.
|
||||
ASN uint
|
||||
// ASOrg overrides the Autonomous System Organization of the geoip data.
|
||||
ASOrg string
|
||||
}
|
||||
231
spn/hub/measurements.go
Normal file
231
spn/hub/measurements.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
// MaxCalculatedCost specifies the max calculated cost to be used for an unknown high cost.
|
||||
const MaxCalculatedCost = 1000000
|
||||
|
||||
// Measurements holds various measurements relating to a Hub.
|
||||
// Fields may not be accessed directly.
|
||||
type Measurements struct {
|
||||
sync.Mutex
|
||||
|
||||
// Latency designates the latency between these Hubs.
|
||||
// It is specified in nanoseconds.
|
||||
Latency time.Duration
|
||||
// LatencyMeasuredAt holds when the latency was measured.
|
||||
LatencyMeasuredAt time.Time
|
||||
|
||||
// Capacity designates the available bandwidth between these Hubs.
|
||||
// It is specified in bit/s.
|
||||
Capacity int
|
||||
// CapacityMeasuredAt holds when the capacity measurement expires.
|
||||
CapacityMeasuredAt time.Time
|
||||
|
||||
// CalculatedCost stores the calculated cost for direct access.
|
||||
// It is not set automatically, but needs to be set when needed.
|
||||
CalculatedCost float32
|
||||
|
||||
// GeoProximity stores an approximation of the geolocation proximity.
|
||||
// The value is between 0 (other side of the world) and 100 (same location).
|
||||
GeoProximity float32
|
||||
|
||||
// persisted holds whether the Measurements have been persisted to the
|
||||
// database.
|
||||
persisted *abool.AtomicBool
|
||||
}
|
||||
|
||||
// NewMeasurements returns a new measurements struct.
|
||||
func NewMeasurements() *Measurements {
|
||||
m := &Measurements{
|
||||
CalculatedCost: MaxCalculatedCost, // Push to back when sorting without data.
|
||||
}
|
||||
m.check()
|
||||
return m
|
||||
}
|
||||
|
||||
// Copy returns a copy of the measurements.
|
||||
func (m *Measurements) Copy() *Measurements {
|
||||
copied := &Measurements{
|
||||
Latency: m.Latency,
|
||||
LatencyMeasuredAt: m.LatencyMeasuredAt,
|
||||
Capacity: m.Capacity,
|
||||
CapacityMeasuredAt: m.CapacityMeasuredAt,
|
||||
CalculatedCost: m.CalculatedCost,
|
||||
}
|
||||
copied.check()
|
||||
return copied
|
||||
}
|
||||
|
||||
// Check checks if the Measurements are properly initialized and ready to use.
|
||||
func (m *Measurements) check() {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if m.persisted == nil {
|
||||
m.persisted = abool.NewBool(true)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPersisted return whether changes to the measurements have been persisted.
|
||||
func (m *Measurements) IsPersisted() bool {
|
||||
return m.persisted.IsSet()
|
||||
}
|
||||
|
||||
// Valid returns whether there is a valid value .
|
||||
func (m *Measurements) Valid() bool {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
switch {
|
||||
case m.Latency == 0:
|
||||
// Latency is not set.
|
||||
case m.Capacity == 0:
|
||||
// Capacity is not set.
|
||||
case m.CalculatedCost == 0:
|
||||
// CalculatedCost is not set.
|
||||
case m.CalculatedCost == MaxCalculatedCost:
|
||||
// CalculatedCost is set to static max value.
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Expired returns whether any of the measurements has expired - calculated
|
||||
// with the given TTL.
|
||||
func (m *Measurements) Expired(ttl time.Duration) bool {
|
||||
expiry := time.Now().Add(-ttl)
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
switch {
|
||||
case expiry.After(m.LatencyMeasuredAt):
|
||||
return true
|
||||
case expiry.After(m.CapacityMeasuredAt):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// SetLatency sets the latency to the given value.
|
||||
func (m *Measurements) SetLatency(latency time.Duration) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
m.Latency = latency
|
||||
m.LatencyMeasuredAt = time.Now()
|
||||
m.persisted.UnSet()
|
||||
}
|
||||
|
||||
// GetLatency returns the latency and when it expires.
|
||||
func (m *Measurements) GetLatency() (latency time.Duration, measuredAt time.Time) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.Latency, m.LatencyMeasuredAt
|
||||
}
|
||||
|
||||
// SetCapacity sets the capacity to the given value.
|
||||
// The capacity is measued in bit/s.
|
||||
func (m *Measurements) SetCapacity(capacity int) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
m.Capacity = capacity
|
||||
m.CapacityMeasuredAt = time.Now()
|
||||
m.persisted.UnSet()
|
||||
}
|
||||
|
||||
// GetCapacity returns the capacity and when it expires.
|
||||
// The capacity is measued in bit/s.
|
||||
func (m *Measurements) GetCapacity() (capacity int, measuredAt time.Time) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.Capacity, m.CapacityMeasuredAt
|
||||
}
|
||||
|
||||
// SetCalculatedCost sets the calculated cost to the given value.
|
||||
// The calculated cost is not set automatically, but needs to be set when needed.
|
||||
func (m *Measurements) SetCalculatedCost(cost float32) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
m.CalculatedCost = cost
|
||||
m.persisted.UnSet()
|
||||
}
|
||||
|
||||
// GetCalculatedCost returns the calculated cost.
|
||||
// The calculated cost is not set automatically, but needs to be set when needed.
|
||||
func (m *Measurements) GetCalculatedCost() (cost float32) {
|
||||
if m == nil {
|
||||
return MaxCalculatedCost
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.CalculatedCost
|
||||
}
|
||||
|
||||
// SetGeoProximity sets the geolocation proximity to the given value.
|
||||
func (m *Measurements) SetGeoProximity(geoProximity float32) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
m.GeoProximity = geoProximity
|
||||
m.persisted.UnSet()
|
||||
}
|
||||
|
||||
// GetGeoProximity returns the geolocation proximity.
|
||||
func (m *Measurements) GetGeoProximity() (geoProximity float32) {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
return m.GeoProximity
|
||||
}
|
||||
|
||||
var (
|
||||
measurementsRegistry = make(map[string]*Measurements)
|
||||
measurementsRegistryLock sync.Mutex
|
||||
)
|
||||
|
||||
func getSharedMeasurements(hubID string, existing *Measurements) *Measurements {
|
||||
measurementsRegistryLock.Lock()
|
||||
defer measurementsRegistryLock.Unlock()
|
||||
|
||||
// 1. Check registry and return shared measurements.
|
||||
m, ok := measurementsRegistry[hubID]
|
||||
if ok {
|
||||
return m
|
||||
}
|
||||
|
||||
// 2. Use existing and make it shared, if available.
|
||||
if existing != nil {
|
||||
existing.check()
|
||||
measurementsRegistry[hubID] = existing
|
||||
return existing
|
||||
}
|
||||
|
||||
// 3. Create new measurements.
|
||||
m = NewMeasurements()
|
||||
measurementsRegistry[hubID] = m
|
||||
return m
|
||||
}
|
||||
308
spn/hub/status.go
Normal file
308
spn/hub/status.go
Normal file
@@ -0,0 +1,308 @@
|
||||
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
|
||||
}
|
||||
152
spn/hub/transport.go
Normal file
152
spn/hub/transport.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Examples:
|
||||
// "spn:17",
|
||||
// "smtp:25",
|
||||
// "smtp:587",
|
||||
// "imap:143",
|
||||
// "http:80",
|
||||
// "http://example.com:80/example", // HTTP (based): use full path for request
|
||||
// "https:443",
|
||||
// "ws:80",
|
||||
// "wss://example.com:443/spn",
|
||||
|
||||
// Transport represents a "endpoint" that others can connect to. This allows for use of different protocols, ports and infrastructure integration.
|
||||
type Transport struct {
|
||||
Protocol string
|
||||
Domain string
|
||||
Port uint16
|
||||
Path string
|
||||
Option string
|
||||
}
|
||||
|
||||
// ParseTransports returns a list of parsed transports and errors from parsing
|
||||
// the given definitions.
|
||||
func ParseTransports(definitions []string) (transports []*Transport, errs []error) {
|
||||
transports = make([]*Transport, 0, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
parsed, err := ParseTransport(definition)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf(
|
||||
"unknown or invalid transport %q: %w", definition, err,
|
||||
))
|
||||
} else {
|
||||
transports = append(transports, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
SortTransports(transports)
|
||||
return transports, errs
|
||||
}
|
||||
|
||||
// ParseTransport parses a transport definition.
|
||||
func ParseTransport(definition string) (*Transport, error) {
|
||||
u, err := url.Parse(definition)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check for invalid parts
|
||||
if u.User != nil {
|
||||
return nil, errors.New("user/pass is not allowed")
|
||||
}
|
||||
|
||||
// put into transport
|
||||
t := &Transport{
|
||||
Protocol: u.Scheme,
|
||||
Domain: u.Hostname(),
|
||||
Path: u.RequestURI(),
|
||||
Option: u.Fragment,
|
||||
}
|
||||
|
||||
// parse port
|
||||
portData := u.Port()
|
||||
if portData == "" {
|
||||
// no port available - it might be in u.Opaque, which holds both the port and possibly a path
|
||||
portData = strings.SplitN(u.Opaque, "/", 2)[0] // get port
|
||||
t.Path = strings.TrimPrefix(t.Path, portData) // trim port from path
|
||||
// check again for port
|
||||
if portData == "" {
|
||||
return nil, errors.New("missing port")
|
||||
}
|
||||
}
|
||||
port, err := strconv.ParseUint(portData, 10, 16)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid port")
|
||||
}
|
||||
t.Port = uint16(port)
|
||||
|
||||
// check port
|
||||
if t.Port == 0 {
|
||||
return nil, errors.New("invalid port")
|
||||
}
|
||||
|
||||
// remove root paths
|
||||
if t.Path == "/" {
|
||||
t.Path = ""
|
||||
}
|
||||
|
||||
// check for protocol
|
||||
if t.Protocol == "" {
|
||||
return nil, errors.New("missing scheme/protocol")
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// String returns the definition form of the transport.
|
||||
func (t *Transport) String() string {
|
||||
switch {
|
||||
case t.Option != "":
|
||||
return fmt.Sprintf("%s://%s:%d%s#%s", t.Protocol, t.Domain, t.Port, t.Path, t.Option)
|
||||
case t.Domain != "":
|
||||
return fmt.Sprintf("%s://%s:%d%s", t.Protocol, t.Domain, t.Port, t.Path)
|
||||
default:
|
||||
return fmt.Sprintf("%s:%d%s", t.Protocol, t.Port, t.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// SortTransports sorts the transports to emphasize certain protocols, but
|
||||
// otherwise leaves the order intact.
|
||||
func SortTransports(ts []*Transport) {
|
||||
slices.SortStableFunc[[]*Transport, *Transport](ts, func(a, b *Transport) int {
|
||||
aOrder := a.protocolOrder()
|
||||
bOrder := b.protocolOrder()
|
||||
|
||||
switch {
|
||||
case aOrder != bOrder:
|
||||
return aOrder - bOrder
|
||||
// case a.Port != b.Port:
|
||||
// return int(a.Port) - int(b.Port)
|
||||
// case a.Domain != b.Domain:
|
||||
// return strings.Compare(a.Domain, b.Domain)
|
||||
// case a.Path != b.Path:
|
||||
// return strings.Compare(a.Path, b.Path)
|
||||
// case a.Option != b.Option:
|
||||
// return strings.Compare(a.Option, b.Option)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Transport) protocolOrder() int {
|
||||
switch t.Protocol {
|
||||
case "http":
|
||||
return 1
|
||||
case "spn":
|
||||
return 2
|
||||
default:
|
||||
return 100
|
||||
}
|
||||
}
|
||||
147
spn/hub/transport_test.go
Normal file
147
spn/hub/transport_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func parseT(t *testing.T, definition string) *Transport {
|
||||
t.Helper()
|
||||
|
||||
tr, err := ParseTransport(definition)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return nil
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
func parseTError(definition string) error {
|
||||
_, err := ParseTransport(definition)
|
||||
return err
|
||||
}
|
||||
|
||||
func TestTransportParsing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// test parsing
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "spn",
|
||||
Port: 17,
|
||||
}, parseT(t, "spn:17"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "smtp",
|
||||
Port: 25,
|
||||
}, parseT(t, "smtp:25"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "smtp",
|
||||
Port: 25,
|
||||
}, parseT(t, "smtp://:25"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "smtp",
|
||||
Port: 587,
|
||||
}, parseT(t, "smtp:587"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "imap",
|
||||
Port: 143,
|
||||
}, parseT(t, "imap:143"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "http",
|
||||
Port: 80,
|
||||
}, parseT(t, "http:80"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "http",
|
||||
Domain: "example.com",
|
||||
Port: 80,
|
||||
}, parseT(t, "http://example.com:80"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "https",
|
||||
Port: 443,
|
||||
}, parseT(t, "https:443"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "ws",
|
||||
Port: 80,
|
||||
}, parseT(t, "ws:80"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "wss",
|
||||
Domain: "example.com",
|
||||
Port: 443,
|
||||
Path: "/spn",
|
||||
}, parseT(t, "wss://example.com:443/spn"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "http",
|
||||
Domain: "example.com",
|
||||
Port: 80,
|
||||
}, parseT(t, "http://example.com:80"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "http",
|
||||
Domain: "example.com",
|
||||
Port: 80,
|
||||
Path: "/test%20test",
|
||||
}, parseT(t, "http://example.com:80/test test"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "http",
|
||||
Domain: "example.com",
|
||||
Port: 80,
|
||||
Path: "/test%20test",
|
||||
}, parseT(t, "http://example.com:80/test%20test"), "should match")
|
||||
|
||||
assert.Equal(t, &Transport{
|
||||
Protocol: "http",
|
||||
Domain: "example.com",
|
||||
Port: 80,
|
||||
Path: "/test?key=value",
|
||||
}, parseT(t, "http://example.com:80/test?key=value"), "should match")
|
||||
|
||||
// test parsing and formatting
|
||||
|
||||
assert.Equal(t, "spn:17",
|
||||
parseT(t, "spn:17").String(), "should match")
|
||||
assert.Equal(t, "smtp:25",
|
||||
parseT(t, "smtp:25").String(), "should match")
|
||||
assert.Equal(t, "smtp:25",
|
||||
parseT(t, "smtp://:25").String(), "should match")
|
||||
assert.Equal(t, "smtp:587",
|
||||
parseT(t, "smtp:587").String(), "should match")
|
||||
assert.Equal(t, "imap:143",
|
||||
parseT(t, "imap:143").String(), "should match")
|
||||
assert.Equal(t, "http:80",
|
||||
parseT(t, "http:80").String(), "should match")
|
||||
assert.Equal(t, "http://example.com:80",
|
||||
parseT(t, "http://example.com:80").String(), "should match")
|
||||
assert.Equal(t, "https:443",
|
||||
parseT(t, "https:443").String(), "should match")
|
||||
assert.Equal(t, "ws:80",
|
||||
parseT(t, "ws:80").String(), "should match")
|
||||
assert.Equal(t, "wss://example.com:443/spn",
|
||||
parseT(t, "wss://example.com:443/spn").String(), "should match")
|
||||
assert.Equal(t, "http://example.com:80",
|
||||
parseT(t, "http://example.com:80").String(), "should match")
|
||||
assert.Equal(t, "http://example.com:80/test%20test",
|
||||
parseT(t, "http://example.com:80/test test").String(), "should match")
|
||||
assert.Equal(t, "http://example.com:80/test%20test",
|
||||
parseT(t, "http://example.com:80/test%20test").String(), "should match")
|
||||
assert.Equal(t, "http://example.com:80/test?key=value",
|
||||
parseT(t, "http://example.com:80/test?key=value").String(), "should match")
|
||||
|
||||
// test invalid
|
||||
|
||||
assert.NotEqual(t, parseTError("spn"), nil, "should fail")
|
||||
assert.NotEqual(t, parseTError("spn:"), nil, "should fail")
|
||||
assert.NotEqual(t, parseTError("spn:0"), nil, "should fail")
|
||||
assert.NotEqual(t, parseTError("spn:65536"), nil, "should fail")
|
||||
}
|
||||
17
spn/hub/truststores.go
Normal file
17
spn/hub/truststores.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package hub
|
||||
|
||||
import "github.com/safing/jess"
|
||||
|
||||
// SingleTrustStore is a simple truststore that always returns the same Signet.
|
||||
type SingleTrustStore struct {
|
||||
Signet *jess.Signet
|
||||
}
|
||||
|
||||
// GetSignet implements the truststore interface.
|
||||
func (ts *SingleTrustStore) GetSignet(id string, recipient bool) (*jess.Signet, error) {
|
||||
if ts.Signet.ID != id || recipient != ts.Signet.Public {
|
||||
return nil, jess.ErrSignetNotFound
|
||||
}
|
||||
|
||||
return ts.Signet, nil
|
||||
}
|
||||
524
spn/hub/update.go
Normal file
524
spn/hub/update.go
Normal file
@@ -0,0 +1,524 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/jess/lhash"
|
||||
"github.com/safing/portbase/container"
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
)
|
||||
|
||||
var (
|
||||
// hubMsgRequirements defines which security attributes message need to have.
|
||||
hubMsgRequirements = jess.NewRequirements().
|
||||
Remove(jess.RecipientAuthentication). // Recipient don't need a private key.
|
||||
Remove(jess.Confidentiality). // Message contents are out in the open.
|
||||
Remove(jess.Integrity) // Only applies to decryption.
|
||||
// SenderAuthentication provides pre-decryption integrity. That is all we need.
|
||||
|
||||
clockSkewTolerance = 1 * time.Hour
|
||||
)
|
||||
|
||||
// SignHubMsg signs the given serialized hub msg with the given configuration.
|
||||
func SignHubMsg(msg []byte, env *jess.Envelope, enableTofu bool) ([]byte, error) {
|
||||
// start session from envelope
|
||||
session, err := env.Correspondence(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initiate signing session: %w", err)
|
||||
}
|
||||
// sign the data
|
||||
letter, err := session.Close(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign msg: %w", err)
|
||||
}
|
||||
|
||||
if enableTofu {
|
||||
// smuggle the public key
|
||||
// letter.Keys is usually only used for key exchanges and encapsulation
|
||||
// neither is used when signing, so we can use letter.Keys to transport public keys
|
||||
for _, sender := range env.Senders {
|
||||
// get public key
|
||||
public, err := sender.AsRecipient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get public key of %s: %w", sender.ID, err)
|
||||
}
|
||||
// serialize key
|
||||
err = public.StoreKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize public key %s: %w", sender.ID, err)
|
||||
}
|
||||
// add to keys
|
||||
letter.Keys = append(letter.Keys, &jess.Seal{
|
||||
Value: public.Key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// pack
|
||||
data, err := letter.ToDSD(dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// OpenHubMsg opens a signed hub msg and verifies the signature using the
|
||||
// provided hub or the local database. If TOFU is enabled, the signature is
|
||||
// always accepted, if valid.
|
||||
func OpenHubMsg(hub *Hub, data []byte, mapName string, tofu bool) (msg []byte, sendingHub *Hub, known bool, err error) {
|
||||
letter, err := jess.LetterFromDSD(data)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("malformed letter: %w", err)
|
||||
}
|
||||
|
||||
// check signatures
|
||||
var seal *jess.Seal
|
||||
switch len(letter.Signatures) {
|
||||
case 0:
|
||||
return nil, nil, false, errors.New("missing signature")
|
||||
case 1:
|
||||
seal = letter.Signatures[0]
|
||||
default:
|
||||
return nil, nil, false, fmt.Errorf("too many signatures (%d)", len(letter.Signatures))
|
||||
}
|
||||
|
||||
// check signature signer ID
|
||||
if seal.ID == "" {
|
||||
return nil, nil, false, errors.New("signature is missing signer ID")
|
||||
}
|
||||
|
||||
// get hub for public key
|
||||
if hub == nil {
|
||||
hub, err = GetHub(mapName, seal.ID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, database.ErrNotFound) {
|
||||
return nil, nil, false, fmt.Errorf("failed to get existing hub %s: %w", seal.ID, err)
|
||||
}
|
||||
hub = nil
|
||||
} else {
|
||||
known = true
|
||||
}
|
||||
} else {
|
||||
known = true
|
||||
}
|
||||
|
||||
var truststore jess.TrustStore
|
||||
if hub != nil && hub.PublicKey != nil { // bootstrap entries will not have a public key
|
||||
// check ID integrity
|
||||
if hub.ID != seal.ID {
|
||||
return nil, hub, known, fmt.Errorf("ID mismatch with hub msg ID %s and hub ID %s", seal.ID, hub.ID)
|
||||
}
|
||||
if !verifyHubID(seal.ID, hub.PublicKey.Scheme, hub.PublicKey.Key) {
|
||||
return nil, hub, known, fmt.Errorf("ID integrity of %s violated with existing key", seal.ID)
|
||||
}
|
||||
} else {
|
||||
if !tofu {
|
||||
return nil, nil, false, fmt.Errorf("hub msg ID %s unknown (missing announcement)", seal.ID)
|
||||
}
|
||||
|
||||
// trust on first use, extract key from keys
|
||||
// TODO: Test if works without TOFU.
|
||||
|
||||
// get key
|
||||
var pubkey *jess.Seal
|
||||
switch len(letter.Keys) {
|
||||
case 0:
|
||||
return nil, nil, false, fmt.Errorf("missing key for TOFU of %s", seal.ID)
|
||||
case 1:
|
||||
pubkey = letter.Keys[0]
|
||||
default:
|
||||
return nil, nil, false, fmt.Errorf("too many keys (%d) for TOFU of %s", len(letter.Keys), seal.ID)
|
||||
}
|
||||
|
||||
// check ID integrity
|
||||
if !verifyHubID(seal.ID, seal.Scheme, pubkey.Value) {
|
||||
return nil, nil, false, fmt.Errorf("ID integrity of %s violated with new key", seal.ID)
|
||||
}
|
||||
|
||||
hub = &Hub{
|
||||
ID: seal.ID,
|
||||
Map: mapName,
|
||||
PublicKey: &jess.Signet{
|
||||
ID: seal.ID,
|
||||
Scheme: seal.Scheme,
|
||||
Key: pubkey.Value,
|
||||
Public: true,
|
||||
},
|
||||
}
|
||||
err = hub.PublicKey.LoadKey()
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// create trust store
|
||||
truststore = &SingleTrustStore{hub.PublicKey}
|
||||
|
||||
// remove keys from letter, as they are only used to transfer the public key
|
||||
letter.Keys = nil
|
||||
|
||||
// check signature
|
||||
err = letter.Verify(hubMsgRequirements, truststore)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
|
||||
return letter.Data, hub, known, nil
|
||||
}
|
||||
|
||||
// Export exports the announcement with the given signature configuration.
|
||||
func (a *Announcement) Export(env *jess.Envelope) ([]byte, error) {
|
||||
// pack
|
||||
msg, err := dsd.Dump(a, dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack announcement: %w", err)
|
||||
}
|
||||
|
||||
return SignHubMsg(msg, env, true)
|
||||
}
|
||||
|
||||
// ApplyAnnouncement applies the announcement to the Hub if it passes all the
|
||||
// checks. If no Hub is provided, it is loaded from the database or created.
|
||||
func ApplyAnnouncement(existingHub *Hub, data []byte, mapName string, scope Scope, selfcheck bool) (hub *Hub, known, changed bool, err error) {
|
||||
// Set valid/invalid status based on the return error.
|
||||
var announcement *Announcement
|
||||
defer func() {
|
||||
if hub != nil {
|
||||
if err != nil && !errors.Is(err, ErrOldData) {
|
||||
hub.InvalidInfo = true
|
||||
} else {
|
||||
hub.InvalidInfo = false
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// open and verify
|
||||
var msg []byte
|
||||
msg, hub, known, err = OpenHubMsg(existingHub, data, mapName, true)
|
||||
|
||||
// Lock hub if we have one.
|
||||
if hub != nil && !selfcheck {
|
||||
hub.Lock()
|
||||
defer hub.Unlock()
|
||||
}
|
||||
|
||||
// Check if there was an error with the Hub msg.
|
||||
if err != nil {
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
// parse
|
||||
announcement = &Announcement{}
|
||||
_, err = dsd.Load(msg, announcement)
|
||||
if err != nil {
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
// integrity check
|
||||
|
||||
// `hub.ID` is taken from the first ever received announcement message.
|
||||
// `announcement.ID` is additionally present in the message as we need
|
||||
// a signed version of the ID to mitigate fake IDs.
|
||||
// Fake IDs are possible because the hash algorithm of the ID is dynamic.
|
||||
if hub.ID != announcement.ID {
|
||||
err = fmt.Errorf("announcement ID %q mismatches hub ID %q", announcement.ID, hub.ID)
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
// version check
|
||||
if hub.Info != nil {
|
||||
// check if we already have this version
|
||||
switch {
|
||||
case announcement.Timestamp == hub.Info.Timestamp && !selfcheck:
|
||||
// The new copy is not saved, as we expect the versions to be identical.
|
||||
// Also, the new version has not been validated at this point.
|
||||
return //nolint:nakedret
|
||||
case announcement.Timestamp < hub.Info.Timestamp:
|
||||
// Received an old version, do not update.
|
||||
err = fmt.Errorf(
|
||||
"%wannouncement from %s @ %s is older than current status @ %s",
|
||||
ErrOldData, hub.StringWithoutLocking(), time.Unix(announcement.Timestamp, 0), time.Unix(hub.Info.Timestamp, 0),
|
||||
)
|
||||
return //nolint:nakedret
|
||||
}
|
||||
}
|
||||
|
||||
// We received a new version.
|
||||
changed = true
|
||||
|
||||
// Update timestamp here already in case validation fails.
|
||||
if hub.Info != nil {
|
||||
hub.Info.Timestamp = announcement.Timestamp
|
||||
}
|
||||
|
||||
// Validate the announcement.
|
||||
err = hub.validateAnnouncement(announcement, scope)
|
||||
if err != nil {
|
||||
if selfcheck || hub.FirstSeen.IsZero() {
|
||||
err = fmt.Errorf("failed to validate announcement of %s: %w", hub.StringWithoutLocking(), err)
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
log.Warningf("spn/hub: received an invalid announcement of %s: %s", hub.StringWithoutLocking(), err)
|
||||
// If a previously fully validated Hub publishes an update that breaks it, a
|
||||
// soft-fail will accept the faulty changes, but mark is as invalid and
|
||||
// forward it to neighbors. This way the invalid update is propagated through
|
||||
// the network and all nodes will mark it as invalid an thus ingore the Hub
|
||||
// until the issue is fixed.
|
||||
}
|
||||
|
||||
// Only save announcement if it is valid.
|
||||
if err == nil {
|
||||
hub.Info = announcement
|
||||
}
|
||||
// Set FirstSeen timestamp when we see this Hub for the first time.
|
||||
if hub.FirstSeen.IsZero() {
|
||||
hub.FirstSeen = time.Now().UTC()
|
||||
}
|
||||
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
func (h *Hub) validateAnnouncement(announcement *Announcement, scope Scope) error {
|
||||
// value formatting
|
||||
if err := announcement.validateFormatting(); err != nil {
|
||||
return err
|
||||
}
|
||||
// check parsables
|
||||
if err := announcement.prepare(true); err != nil {
|
||||
return fmt.Errorf("failed to prepare announcement: %w", err)
|
||||
}
|
||||
|
||||
// check timestamp
|
||||
if announcement.Timestamp > time.Now().Add(clockSkewTolerance).Unix() {
|
||||
return fmt.Errorf(
|
||||
"announcement from %s @ %s is from the future",
|
||||
announcement.ID,
|
||||
time.Unix(announcement.Timestamp, 0),
|
||||
)
|
||||
}
|
||||
|
||||
// check for illegal IP address changes
|
||||
if h.Info != nil {
|
||||
switch {
|
||||
case h.Info.IPv4 != nil && announcement.IPv4 == nil:
|
||||
h.VerifiedIPs = false
|
||||
return errors.New("previously announced IPv4 address missing")
|
||||
case h.Info.IPv4 != nil && !announcement.IPv4.Equal(h.Info.IPv4):
|
||||
h.VerifiedIPs = false
|
||||
return errors.New("IPv4 address changed")
|
||||
case h.Info.IPv6 != nil && announcement.IPv6 == nil:
|
||||
h.VerifiedIPs = false
|
||||
return errors.New("previously announced IPv6 address missing")
|
||||
case h.Info.IPv6 != nil && !announcement.IPv6.Equal(h.Info.IPv6):
|
||||
h.VerifiedIPs = false
|
||||
return errors.New("IPv6 address changed")
|
||||
}
|
||||
}
|
||||
|
||||
// validate IP scopes
|
||||
if announcement.IPv4 != nil {
|
||||
ipScope := netutils.GetIPScope(announcement.IPv4)
|
||||
switch {
|
||||
case scope == ScopeLocal && !ipScope.IsLAN():
|
||||
return errors.New("IPv4 scope violation: outside of local scope")
|
||||
case scope == ScopePublic && !ipScope.IsGlobal():
|
||||
return errors.New("IPv4 scope violation: outside of global scope")
|
||||
}
|
||||
// Reset IP verification flag if IPv4 was added.
|
||||
if h.Info == nil || h.Info.IPv4 == nil {
|
||||
h.VerifiedIPs = false
|
||||
}
|
||||
}
|
||||
if announcement.IPv6 != nil {
|
||||
ipScope := netutils.GetIPScope(announcement.IPv6)
|
||||
switch {
|
||||
case scope == ScopeLocal && !ipScope.IsLAN():
|
||||
return errors.New("IPv6 scope violation: outside of local scope")
|
||||
case scope == ScopePublic && !ipScope.IsGlobal():
|
||||
return errors.New("IPv6 scope violation: outside of global scope")
|
||||
}
|
||||
// Reset IP verification flag if IPv6 was added.
|
||||
if h.Info == nil || h.Info.IPv6 == nil {
|
||||
h.VerifiedIPs = false
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Export exports the status with the given signature configuration.
|
||||
func (s *Status) Export(env *jess.Envelope) ([]byte, error) {
|
||||
// pack
|
||||
msg, err := dsd.Dump(s, dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pack status: %w", err)
|
||||
}
|
||||
|
||||
return SignHubMsg(msg, env, false)
|
||||
}
|
||||
|
||||
// ApplyStatus applies a status update if it passes all the checks.
|
||||
func ApplyStatus(existingHub *Hub, data []byte, mapName string, scope Scope, selfcheck bool) (hub *Hub, known, changed bool, err error) {
|
||||
// Set valid/invalid status based on the return error.
|
||||
defer func() {
|
||||
if hub != nil {
|
||||
if err != nil && !errors.Is(err, ErrOldData) {
|
||||
hub.InvalidStatus = true
|
||||
} else {
|
||||
hub.InvalidStatus = false
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// open and verify
|
||||
var msg []byte
|
||||
msg, hub, known, err = OpenHubMsg(existingHub, data, mapName, false)
|
||||
|
||||
// Lock hub if we have one.
|
||||
if hub != nil && !selfcheck {
|
||||
hub.Lock()
|
||||
defer hub.Unlock()
|
||||
}
|
||||
|
||||
// Check if there was an error with the Hub msg.
|
||||
if err != nil {
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
// parse
|
||||
status := &Status{}
|
||||
_, err = dsd.Load(msg, status)
|
||||
if err != nil {
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
// version check
|
||||
if hub.Status != nil {
|
||||
// check if we already have this version
|
||||
switch {
|
||||
case status.Timestamp == hub.Status.Timestamp && !selfcheck:
|
||||
// The new copy is not saved, as we expect the versions to be identical.
|
||||
// Also, the new version has not been validated at this point.
|
||||
return //nolint:nakedret
|
||||
case status.Timestamp < hub.Status.Timestamp:
|
||||
// Received an old version, do not update.
|
||||
err = fmt.Errorf(
|
||||
"%wstatus from %s @ %s is older than current status @ %s",
|
||||
ErrOldData, hub.StringWithoutLocking(), time.Unix(status.Timestamp, 0), time.Unix(hub.Status.Timestamp, 0),
|
||||
)
|
||||
return //nolint:nakedret
|
||||
}
|
||||
}
|
||||
|
||||
// We received a new version.
|
||||
changed = true
|
||||
|
||||
// Update timestamp here already in case validation fails.
|
||||
if hub.Status != nil {
|
||||
hub.Status.Timestamp = status.Timestamp
|
||||
}
|
||||
|
||||
// Validate the status.
|
||||
err = hub.validateStatus(status)
|
||||
if err != nil {
|
||||
if selfcheck {
|
||||
err = fmt.Errorf("failed to validate status of %s: %w", hub.StringWithoutLocking(), err)
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
log.Warningf("spn/hub: received an invalid status of %s: %s", hub.StringWithoutLocking(), err)
|
||||
// If a previously fully validated Hub publishes an update that breaks it, a
|
||||
// soft-fail will accept the faulty changes, but mark is as invalid and
|
||||
// forward it to neighbors. This way the invalid update is propagated through
|
||||
// the network and all nodes will mark it as invalid an thus ingore the Hub
|
||||
// until the issue is fixed.
|
||||
}
|
||||
|
||||
// Only save status if it is valid, else mark it as invalid.
|
||||
if err == nil {
|
||||
hub.Status = status
|
||||
}
|
||||
|
||||
return //nolint:nakedret
|
||||
}
|
||||
|
||||
func (h *Hub) validateStatus(status *Status) error {
|
||||
// value formatting
|
||||
if err := status.validateFormatting(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check timestamp
|
||||
if status.Timestamp > time.Now().Add(clockSkewTolerance).Unix() {
|
||||
return fmt.Errorf(
|
||||
"status from %s @ %s is from the future",
|
||||
h.ID,
|
||||
time.Unix(status.Timestamp, 0),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: validate status.Keys
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateHubSignet creates a signet with the correct ID for usage as a Hub Identity.
|
||||
func CreateHubSignet(toolID string, securityLevel int) (private, public *jess.Signet, err error) {
|
||||
private, err = jess.GenerateSignet(toolID, securityLevel)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate key: %w", err)
|
||||
}
|
||||
err = private.StoreKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to store private key: %w", err)
|
||||
}
|
||||
|
||||
// get public key for creating the Hub ID
|
||||
public, err = private.AsRecipient()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
err = public.StoreKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to store public key: %w", err)
|
||||
}
|
||||
|
||||
// assign IDs
|
||||
private.ID = createHubID(public.Scheme, public.Key)
|
||||
public.ID = private.ID
|
||||
|
||||
return private, public, nil
|
||||
}
|
||||
|
||||
func createHubID(scheme string, pubkey []byte) string {
|
||||
// compile scheme and public key
|
||||
c := container.New()
|
||||
c.AppendAsBlock([]byte(scheme))
|
||||
c.AppendAsBlock(pubkey)
|
||||
|
||||
return lhash.Digest(lhash.BLAKE2b_256, c.CompileData()).Base58()
|
||||
}
|
||||
|
||||
func verifyHubID(id string, scheme string, pubkey []byte) (ok bool) {
|
||||
// load labeled hash from ID
|
||||
labeledHash, err := lhash.FromBase58(id)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// compile scheme and public key
|
||||
c := container.New()
|
||||
c.AppendAsBlock([]byte(scheme))
|
||||
c.AppendAsBlock(pubkey)
|
||||
|
||||
// check if it matches
|
||||
return labeledHash.MatchesData(c.CompileData())
|
||||
}
|
||||
70
spn/hub/update_test.go
Normal file
70
spn/hub/update_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
)
|
||||
|
||||
func TestHubUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// message signing
|
||||
|
||||
testData := []byte{0}
|
||||
|
||||
s1, err := jess.GenerateSignet("Ed25519", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = s1.StoreKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Printf("s1: %+v\n", s1)
|
||||
|
||||
s1e, err := s1.AsRecipient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = s1e.StoreKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s1e.ID = createHubID(s1e.Scheme, s1e.Key)
|
||||
s1.ID = s1e.ID
|
||||
|
||||
t.Logf("generated hub ID: %s", s1.ID)
|
||||
|
||||
env := jess.NewUnconfiguredEnvelope()
|
||||
env.SuiteID = jess.SuiteSignV1
|
||||
env.Senders = []*jess.Signet{s1}
|
||||
|
||||
s, err := env.Correspondence(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
letter, err := s.Close(testData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// smuggle the key
|
||||
letter.Keys = append(letter.Keys, &jess.Seal{
|
||||
Value: s1e.Key,
|
||||
})
|
||||
t.Logf("letter with smuggled key: %+v", letter)
|
||||
|
||||
// pack
|
||||
data, err := letter.ToDSD(dsd.JSON)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, _, _, err = OpenHubMsg(nil, data, "test", true) //nolint:dogsled
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user