Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions caerulean/whirlpool/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ENV SEASIDE_API_PORT=8587
ENV SEASIDE_CERTIFICATE_PATH=certificates
ENV SEASIDE_LOG_PATH=logs

ENV SEASIDE_MAX_DEVICES=1
ENV SEASIDE_MAX_VIRIDIANS=10
ENV SEASIDE_MAX_ADMINS=5

Expand Down
2 changes: 2 additions & 0 deletions caerulean/whirlpool/example.conf.env
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ SEASIDE_TYPHOON_PORT=29384
# DNS server that will be suggested to the clients (although not enforced), can be any local or global reliable DNS or 0.0.0.0 for using the current one
SEASIDE_SUGGESTED_DNS=0.0.0.0

# Maximum number of devices every viridian is allowed to connect simultaneously
SEASIDE_MAX_DEVICES=1
# Maximum network viridian number (should be >= 0)
SEASIDE_MAX_VIRIDIANS=10
# Maximum privileged viridian number (should be >= 0)
Expand Down
26 changes: 23 additions & 3 deletions caerulean/whirlpool/sources/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"main/crypto"
"main/generated"
"main/users"
"main/utils"
"net"
"os"
Expand Down Expand Up @@ -40,6 +41,8 @@ var (

GRPC_MAX_TAIL_LENGTH = uint(utils.GetIntEnv("SEASIDE_GRPC_MAX_TAIL_LENGTH", DEFAULT_GRPC_MAX_TAIL_LENGTH, 32))
SUGGESTED_DNS_SERVER = utils.GetEnv("SEASIDE_SUGGESTED_DNS", DEFAULT_SUGGESTED_DNS)

MAX_DEVICES = uint16(utils.GetIntEnv("SEASIDE_MAX_DEVICES", users.DEFAULT_MAX_DEVICES, 16))
)

// Metaserver structure.
Expand Down Expand Up @@ -166,18 +169,35 @@ func (server *APIServer) Stop() {
// Return authentication response and nil if authentication successful, otherwise nil and error.
func (server *WhirlpoolServer) Authenticate(ctx context.Context, request *generated.WhirlpoolAuthenticationRequest) (*generated.WhirlpoolAuthenticationResponse, error) {
// Check node owner or viridian payload
if request.ApiKey != NODE_OWNER_API_KEY && !slices.Contains(NODE_ADMIN_API_KEYS, request.ApiKey) {
calledByOwner := request.ApiKey == NODE_OWNER_API_KEY
calledByAdmin := slices.Contains(NODE_ADMIN_API_KEYS, request.ApiKey)

// Raise an error if not called by authority
if !calledByOwner && !calledByAdmin {
return nil, status.Error(codes.PermissionDenied, "wrong payload value")
}

// Downgrade privileges if called not by owner
if !calledByOwner {
request.Privileged = false
}

// Limit device number to the maximum allowed (if set)
if request.Devices == nil {
*request.Devices = uint32(MAX_DEVICES)
} else {
*request.Devices = min(uint32(MAX_DEVICES), *request.Devices)
}

// Create and marshall user token (will be valid for 10 years for non-privileged users)
token := &generated.UserToken{
Name: request.Name,
Identifier: request.Identifier,
IsAdmin: true,
IsAdmin: request.Privileged,
Subscription: request.Subscription,
Devices: request.Devices,
}
logrus.Infof("User %s (id: %s) autnenticated", token.Name, token.Identifier)
logrus.Infof("User %s (id: %s) authenticated", token.Name, token.Identifier)
marshToken, err := proto.Marshal(token)
if err != nil {
return nil, status.Errorf(codes.Internal, "error marshalling token: %v", err)
Expand Down
3 changes: 2 additions & 1 deletion caerulean/whirlpool/tunnel/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ type TunnelConfig struct {
// Preserve current iptables configuration in a TunnelConfig object.
// Create and return the tunnel config pointer.
func Preserve() (*TunnelConfig, error) {
maxDevices := utils.GetIntEnv("SEASIDE_MAX_DEVICES", users.DEFAULT_MAX_DEVICES, 32)
maxViridians := utils.GetIntEnv("SEASIDE_MAX_VIRIDIANS", users.DEFAULT_MAX_VIRIDIANS, 32)
maxAdmins := utils.GetIntEnv("SEASIDE_MAX_ADMINS", users.DEFAULT_MAX_ADMINS, 32)
maxTotal := int32(maxViridians + maxAdmins)
maxTotal := int32((maxViridians + maxAdmins) * maxDevices)
burstMultiplier := uint32(utils.GetIntEnv("SEASIDE_BURST_LIMIT_MULTIPLIER", DEFAULT_BURST_MULTIPLIER, 32))

defaultNet, err := findDefaultInterface()
Expand Down
47 changes: 34 additions & 13 deletions caerulean/whirlpool/users/dictionary.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
)

const (
DEFAULT_MAX_DEVICES = 1
DEFAULT_MAX_VIRIDIANS = 10
DEFAULT_MAX_ADMINS = 5
)
Expand All @@ -28,8 +29,8 @@ type ViridianDict struct {
// The viridian dictionary itself.
entries map[uint16]*Viridian

// The viridian dictionary re-mapped by unique IDs.
uniques map[string]*Viridian
// The viridian token unique IDs mapped to the number of devices currently connected.
uniques map[string]uint16

// Mutex for viridian operations.
mutex sync.Mutex
Expand All @@ -40,21 +41,23 @@ type ViridianDict struct {
// Accept context, return viridian dictionary pointer.
func NewViridianDict() (*ViridianDict, error) {
// Retrieve limits from environment variables
maxDevices := uint16(utils.GetIntEnv("SEASIDE_MAX_DEVICES", DEFAULT_MAX_DEVICES, 16))
maxViridians := uint16(utils.GetIntEnv("SEASIDE_MAX_VIRIDIANS", DEFAULT_MAX_VIRIDIANS, 16))
maxAdmins := uint16(utils.GetIntEnv("SEASIDE_MAX_ADMINS", DEFAULT_MAX_ADMINS, 16))
maxTotal := maxViridians + maxAdmins
maxEntries := maxTotal * maxDevices

// Exit if limit configuration is inconsistent
if maxTotal > math.MaxUint16-3 {
return nil, fmt.Errorf("error initializing viridian array: too many users requested: %d", maxTotal)
if maxEntries > math.MaxUint16-3 {
return nil, fmt.Errorf("error initializing viridian array: too many users requested: %d", maxEntries)
}

// Create viridian dictionary object and start sending packets to them
dict := ViridianDict{
maxViridians: maxViridians,
maxOverhead: maxAdmins,
entries: make(map[uint16]*Viridian, maxTotal),
uniques: make(map[string]*Viridian, maxTotal),
entries: make(map[uint16]*Viridian, maxEntries),
uniques: make(map[string]uint16, maxTotal),
}

// Return dictionary pointer
Expand All @@ -76,10 +79,9 @@ func (dict *ViridianDict) Add(getViridianID func() (any, uint16, error), viridia
defer dict.mutex.Unlock()

// Check if there are slots available (or if viridian is already connected)
viridian, ok := dict.uniques[token.Identifier]
if ok {
viridian.stop()
delete(dict.entries, viridian.peerID)
connected, ok := dict.uniques[token.Identifier] // TODO: check if returns 0 by default
if ok && *token.Devices <= uint32(connected) {
return nil, 0, fmt.Errorf("can not connect any more viridians with this token, connected: %d", connected)
} else if !token.IsAdmin && len(dict.entries) >= int(dict.maxViridians) {
return nil, 0, fmt.Errorf("can not connect any more viridians, connected: %d", len(dict.entries))
} else if len(dict.entries) == int(dict.maxViridians+dict.maxOverhead) {
Expand Down Expand Up @@ -114,7 +116,7 @@ func (dict *ViridianDict) Add(getViridianID func() (any, uint16, error), viridia
}

// Create viridian object
viridian = &Viridian{
viridian := &Viridian{
Name: token.Name,
Device: *viridianDevice,
Identifier: token.Identifier,
Expand All @@ -125,7 +127,7 @@ func (dict *ViridianDict) Add(getViridianID func() (any, uint16, error), viridia
}

dict.entries[viridianID] = viridian
dict.uniques[token.Identifier] = viridian
dict.uniques[token.Identifier] = connected + 1
return viridianHandle, viridianID, nil
}

Expand All @@ -150,13 +152,28 @@ func (dict *ViridianDict) Delete(viridianID uint16, timeout bool) {
// Retrieve viridian from the dictionary
viridian, ok := dict.entries[viridianID]
if !ok {
logrus.Warnf("User %d should have been deleted, but was not found!", viridianID)
return
}

// Stop viridian and remove it from the dictionary
viridian.stop()
delete(dict.entries, viridianID)

// Retrieve connected devices number
connected, ok := dict.uniques[viridian.Identifier]
if !ok {
logrus.Warnf("User %d was deleted, but was not connected!", viridianID)
return
}

// Remove viridian from the uniques dictionary
if connected > 1 {
dict.uniques[viridian.Identifier] = connected - 1
} else {
delete(dict.uniques, viridian.Identifier)
}

// Log appropriate message if deleted by timeout
if timeout {
logrus.Infof("User %d deleted because their subscription has expired!", viridianID)
Expand All @@ -165,7 +182,7 @@ func (dict *ViridianDict) Delete(viridianID uint16, timeout bool) {
}
}

// Clear viridan dictionary.
// Clear viridian dictionary.
// Stop all viridian connections and delete all the objects.
// Should be applied for ViridianDict object.
func (dict *ViridianDict) Clear() {
Expand All @@ -176,4 +193,8 @@ func (dict *ViridianDict) Clear() {
viridian.stop()
delete(dict.entries, key)
}

for key := range dict.uniques {
delete(dict.uniques, key)
}
}
2 changes: 2 additions & 0 deletions vessels/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ message UserToken {
bool is_admin = 3;
// User subscription end timestamp
optional google.protobuf.Timestamp subscription = 4;
// Device number
optional uint32 devices = 5;
}
11 changes: 8 additions & 3 deletions vessels/whirlpool_viridian.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import "google/protobuf/timestamp.proto";

option go_package = "/generated";


// TODO: get general node info
// TODO: get user key info

// User data that is needed for administrator whirlpool node authentication
message WhirlpoolAuthenticationRequest {
Expand All @@ -14,9 +15,13 @@ message WhirlpoolAuthenticationRequest {
// User unique identifier
string identifier = 2;

string api_key = 3;
bool privileged = 3;

string api_key = 4;

optional google.protobuf.Timestamp subscription = 5;

optional google.protobuf.Timestamp subscription = 4;
optional uint32 devices = 6;
}

// User authentication certificate
Expand Down
3 changes: 3 additions & 0 deletions viridian/algae/setup/whirlpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
_DEFAULT_SUGGESTED_DNS = "8.8.8.8"
_DEFAULT_CERTIFICATES_PATH = "certificates"
_DEFAULT_LOG_PATH = "log"
_DEFAULT_MAX_DEVICES = 1
_DEFAULT_MAX_VIRIDIANS = 10
_DEFAULT_MAX_ADMINS = 5
_DEFAULT_WAITING_OVERTIME = 15
Expand Down Expand Up @@ -89,6 +90,7 @@ def create_parser(cls, subparser: "_SubParsersAction[ArgumentParser]") -> None:
parser.add_argument("--typhoon-port", type=port_number(_MIN_PORT_VALUE, _MAX_PORT_VALUE), default=DEFAULT_GENERATED_VALUE, help=f"Seaside control port number (default: random, between {_MIN_PORT_VALUE} and {_MAX_PORT_VALUE})")
parser.add_argument("--certificates-path", type=str, default=_DEFAULT_CERTIFICATES_PATH, help=f"Path for storing certificates, two files should be present there, 'cert.crt' and 'key.crt' (default: {_DEFAULT_CERTIFICATES_PATH})")
parser.add_argument("--suggested-dns", type=current_dns(_DEFAULT_SUGGESTED_DNS), default=DEFAULT_GENERATED_VALUE, help=f"Path for storing certificates, two files should be present there, 'cert.crt' and 'key.crt' (default: {_DEFAULT_CERTIFICATES_PATH})")
parser.add_argument("--max-devices", type=int, default=_DEFAULT_MAX_DEVICES, help=f"Maximum devices for every viridian (default: {_DEFAULT_MAX_DEVICES})")
parser.add_argument("--max-viridians", type=int, default=_DEFAULT_MAX_VIRIDIANS, help=f"Maximum network viridian number (default: {_DEFAULT_MAX_VIRIDIANS})")
parser.add_argument("--max-admins", type=int, default=_DEFAULT_MAX_ADMINS, help=f"Maximum privileged viridian number (default: {_DEFAULT_MAX_ADMINS})")
parser.add_argument("--tunnel-mtu", type=int, default=_DEFAULT_TUNNEL_MTU, help=f"VPN tunnel interface MTU (default: {_DEFAULT_TUNNEL_MTU})")
Expand Down Expand Up @@ -131,6 +133,7 @@ def create_environment(self) -> Dict[str, str]:
environment["SEASIDE_TYPHOON_PORT"] = self._args["typhoon_port"]
environment["SEASIDE_CERTIFICATE_PATH"] = self._args["certificates_path"]
environment["SEASIDE_SUGGESTED_DNS"] = self._args["suggested_dns"]
environment["SEASIDE_MAX_DEVICES"] = self._args["max_devices"]
environment["SEASIDE_MAX_VIRIDIANS"] = self._args["max_viridians"]
environment["SEASIDE_MAX_ADMINS"] = self._args["max_admins"]
environment["SEASIDE_TUNNEL_MTU"] = self._args["tunnel_mtu"]
Expand Down
12 changes: 9 additions & 3 deletions viridian/algae/sources/automation/whirlpool_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@

_DEFAULT_NAME = gethostname()

_DEFAULT_PRIVILEGED = False

_DEFAULT_SUBSCRIPTION = 365

_DEFAULT_DEVICES = None

logger = create_logger(__name__)


Expand All @@ -35,19 +39,21 @@
supply_viridian_parser = subparsers.add_parser("supply-viridian", help="Add viridian (owner or admin) to the server and print their token")
supply_viridian_parser.add_argument("-i", "--identifier", default=_DEFAULT_IDENTIFIER, help=f"Viridian unique identifier (default: {_DEFAULT_IDENTIFIER})")
supply_viridian_parser.add_argument("-n", "--name", default=_DEFAULT_NAME, help=f"Viridian non-unique name (default is equal to the device name: {_DEFAULT_NAME})")
supply_viridian_parser.add_argument("-p", "--privileged", default=_DEFAULT_PRIVILEGED, help=f"Viridian is administrator (only effective if key is owner key, default: {_DEFAULT_PRIVILEGED})")
supply_viridian_parser.add_argument("-d", "--days", default=_DEFAULT_SUBSCRIPTION, help=f"Viridian subscription length, in days (default: {_DEFAULT_SUBSCRIPTION})")
supply_viridian_parser.add_argument("-v", "--devices", default=_DEFAULT_DEVICES, help=f"Viridian devices number for simultaneous connection (default: {_DEFAULT_DEVICES})")
supply_viridian_parser.add_argument("-s", "--silent", action="store_true", default=False, help="Only output the API token and no logs, used for automatization (other useful info like public key pr protocol ports won't be displayed, default: False)")


async def supply_viridian(address: str, port: int, key: str, identifier: str, name: Optional[str], days: int, silent: bool) -> None:
async def supply_viridian(address: str, port: int, key: str, identifier: str, name: Optional[str], privileged: bool, days: int, devices: Optional[int], silent: bool) -> None:
logger.disabled = silent

authority = getenv("SEASIDE_CERTIFICATE_PATH", None)
logger.info(f"Starting client with CA certificate located at: {authority}...")
client = WhirlpoolClient(address, port, Path(authority))

logger.info(f"Authenticating user {identifier} (key {key}, name {name}, subscription {days})...")
public, token, typhoon_port, port_port, dns = await client.authenticate(identifier, key, name, days)
logger.info(f"Authenticating user {identifier} (key {key}, name {name}, privileged {privileged}, subscription {days}, devices {devices})...")
public, token, typhoon_port, port_port, dns = await client.authenticate(identifier, key, name, privileged, days, devices)
logger.info(f"Caerulean connection info received: public key {encodebytes(public)!r}, TYPHOON port {typhoon_port}, PORT port {port_port}, DNS {dns}")

if silent:
Expand Down
7 changes: 4 additions & 3 deletions viridian/algae/sources/interaction/whirlpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ..utils.misc import create_logger, random_number
from .generated import WhirlpoolAuthenticationRequest, WhirlpoolViridianStub

_DEFAULT_PRIVILEGED = False
_DEFAULT_SUBSCRIPTION_DAYS = 30
_METADATA_TAIL_MAX = 1024
_DEFAULT_TIMEOUT = 30
Expand Down Expand Up @@ -52,11 +53,11 @@ def _create_grpc_secure_channel(self, host: str, port: int, cert_path: Optional[
context.set_alpn_protocols(["h2", "http/1.1"])
return Channel(host, port, ssl=context)

async def authenticate(self, identifier: str, api_key: str, name: Optional[str] = None, subscription: int = _DEFAULT_SUBSCRIPTION_DAYS) -> Tuple[bytes, bytes, int, int, str]:
async def authenticate(self, identifier: str, api_key: str, name: Optional[str] = None, privileged: bool = _DEFAULT_PRIVILEGED, subscription: int = _DEFAULT_SUBSCRIPTION_DAYS, devices: Optional[int] = None) -> Tuple[bytes, bytes, int, int, str]:
name = gethostname() if name is None else name
subscription = datetime.now(timezone.utc) + timedelta(days=subscription)
logger.debug(f"User will be initiated with name '{name}', subscription until {subscription} and identifier: {identifier}!")
response = await super().authenticate(WhirlpoolAuthenticationRequest(name, identifier, api_key, subscription))
logger.debug(f"User will be initiated with name '{name}' privileged {privileged}, subscription until {subscription} with {devices} devices and identifier: {identifier}!")
response = await super().authenticate(WhirlpoolAuthenticationRequest(name, identifier, privileged, api_key, subscription, devices))
logger.debug(f"Symmetric session token received: {response.token!r}!")
return response.public_key, response.token, response.typhoon_port, response.port_port, response.dns

Expand Down
Loading