mirror of
https://github.com/azukaar/Cosmos-Server.git
synced 2026-10-12 02:11:26 +00:00
[release] v0.22.32
This commit is contained in:
+12
@@ -38,3 +38,15 @@ terraform.tfstate.*
|
||||
.terraform.lock.hcl
|
||||
*.log
|
||||
./main.tf
|
||||
|
||||
# Ruflo local secrets and runtime data
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.claude-flow/
|
||||
.claude/
|
||||
.swarm/
|
||||
.mcp.json
|
||||
ruvector.db
|
||||
tickets/
|
||||
CLAUDE.md
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "REST API for Cosmos Cloud server management",
|
||||
"title": "Cosmos Server API",
|
||||
"contact": {},
|
||||
"version": "0.22.31"
|
||||
"version": "0.22.32"
|
||||
},
|
||||
"paths": {
|
||||
"/_logs": {
|
||||
@@ -10702,9 +10702,6 @@
|
||||
},
|
||||
"port": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "REST API for Cosmos Cloud server management",
|
||||
"title": "Cosmos Server API",
|
||||
"contact": {},
|
||||
"version": "0.22.31"
|
||||
"version": "0.22.32"
|
||||
},
|
||||
"basePath": "/cosmos",
|
||||
"paths": {
|
||||
@@ -8779,9 +8779,6 @@
|
||||
},
|
||||
"port": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -146,8 +146,6 @@ definitions:
|
||||
type: string
|
||||
port:
|
||||
type: string
|
||||
publicKey:
|
||||
type: string
|
||||
required:
|
||||
- deviceName
|
||||
- ip
|
||||
@@ -2063,7 +2061,7 @@ info:
|
||||
contact: {}
|
||||
description: REST API for Cosmos Cloud server management
|
||||
title: Cosmos Server API
|
||||
version: 0.22.31
|
||||
version: 0.22.32
|
||||
paths:
|
||||
/_logs:
|
||||
get:
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
## Version 0.22.32
|
||||
- Fix Constellation CA certificates being generated with a 1 year expiry instead of 10
|
||||
- Start NATS on standalone Constellation servers so clients can fetch the device list
|
||||
- Fix permission check when blocking Constellation devices
|
||||
- Harden Constellation DNS (crash fix, stricter hostname matching)
|
||||
- Fix Constellation device private keys being stored and returned by the API as PublicKey
|
||||
- Additional Constellation fixes and cleanups
|
||||
- Multiple bug fixes on the constellation client
|
||||
- windows duplicate window
|
||||
- Linux errors
|
||||
- MacOS DNSes
|
||||
- Mobile battery drain
|
||||
|
||||
## Version 0.22.31
|
||||
- Update to latest LEGO
|
||||
- Solve issue where removing a licence would cause errors spam
|
||||
|
||||
@@ -60,9 +60,9 @@ function addDevice(device) {
|
||||
"LighthousesList": [],
|
||||
"Nickname": "admin",
|
||||
"Port": "4242",
|
||||
"PrivateKey": "-----BEGIN NEBULA CERTIFICATE-----\...//w8o3ZaFqQYwhdGFuAY6IGXmYRCr3z932Y....w\..==\n-----END NEBULA CERTIFICATE-----\n",
|
||||
"PublicHostname": "",
|
||||
"PublicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\nnS39dWX...hTvP......+43thNrMu7U=\n-----END NEBULA X25519 PRIVATE KEY-----\n"
|
||||
"Certificate": "-----BEGIN NEBULA CERTIFICATE-----\...//w8o3ZaFqQYwhdGFuAY6IGXmYRCr3z932Y....w\..==\n-----END NEBULA CERTIFICATE-----\n",
|
||||
"PrivateKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\nnS39dWX...hTvP......+43thNrMu7U=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
|
||||
"PublicHostname": ""
|
||||
},
|
||||
"status": "OK"
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import wrap, { type ApiResponse, type ApiFetch } from './wrap';
|
||||
export interface ConstellationDevice {
|
||||
Nickname: string;
|
||||
DeviceName: string;
|
||||
PublicKey: string;
|
||||
IP: string;
|
||||
IsLighthouse: boolean;
|
||||
IsRelay: boolean;
|
||||
|
||||
+312
-313
@@ -311,9 +311,8 @@ type ConstellationDeviceCreateRequestJSON struct {
|
||||
IsRelay *bool `json:"isRelay,omitempty"`
|
||||
|
||||
// Nickname for devices only
|
||||
Nickname *string `json:"nickname,omitempty"`
|
||||
Port *string `json:"port,omitempty"`
|
||||
PublicKey *string `json:"publicKey,omitempty"`
|
||||
Nickname *string `json:"nickname,omitempty"`
|
||||
Port *string `json:"port,omitempty"`
|
||||
}
|
||||
|
||||
// ConstellationDeviceEditRequestJSON defines model for constellation.DeviceEditRequestJSON.
|
||||
@@ -36101,316 +36100,316 @@ func ParsePostRcloneVfsStatsResponse(rsp *http.Response) (*PostRcloneVfsStatsRes
|
||||
// const string: with thousands of chunks the chained `+` fold is several
|
||||
// times slower for the Go compiler than parsing a slice literal.
|
||||
var swaggerSpec = []string{
|
||||
"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",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cosmossdk
|
||||
|
||||
// Version is the SDK version, synced with Cosmos Server at build time.
|
||||
const Version = "0.22.31"
|
||||
const Version = "0.22.32"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.22.31",
|
||||
"version": "0.22.32",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
||||
+124
-42
@@ -1,13 +1,15 @@
|
||||
package constellation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
var DNSBlacklist = map[string]bool{}
|
||||
@@ -24,7 +26,19 @@ func externalLookup(client *dns.Client, r *dns.Msg, serverAddr string) (*dns.Msg
|
||||
return client.Exchange(rCopy, serverAddr)
|
||||
}
|
||||
|
||||
// matchesDomain reports whether qName matches hostname exactly or on a label boundary
|
||||
func matchesDomain(qName string, hostname string) bool {
|
||||
return qName == hostname + "." || strings.HasSuffix(qName, "." + hostname + ".")
|
||||
}
|
||||
|
||||
func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
if len(r.Question) == 0 {
|
||||
m := new(dns.Msg)
|
||||
m.SetRcode(r, dns.RcodeFormatError)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
config := utils.GetMainConfig()
|
||||
DNSFallback := config.ConstellationConfig.DNSFallback
|
||||
|
||||
@@ -52,7 +66,7 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
hostname := entry.Key
|
||||
ip := entry.Value
|
||||
|
||||
if strings.HasSuffix(q.Name, hostname + ".") && q.Qtype == dns.TypeA {
|
||||
if matchesDomain(q.Name, hostname) && q.Qtype == dns.TypeA {
|
||||
utils.Debug("DNS Overwrite " + hostname + " with " + ip)
|
||||
rr, _ := dns.NewRR(q.Name + " A " + ip)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
@@ -71,7 +85,7 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
for _, q := range r.Question {
|
||||
utils.Debug("DNS Question " + q.Name)
|
||||
for _, hostname := range hostnames {
|
||||
if strings.HasSuffix(q.Name, hostname + ".") && q.Qtype == dns.TypeA {
|
||||
if matchesDomain(q.Name, hostname) && q.Qtype == dns.TypeA {
|
||||
utils.Debug("DNS Overwrite " + hostname + " with " + thisIp)
|
||||
rr, _ := dns.NewRR(q.Name + " A " + thisIp)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
@@ -94,7 +108,7 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
}
|
||||
destination := CachedDeviceNames[target.DeviceName]
|
||||
if destination != "" {
|
||||
if strings.HasSuffix(q.Name, tunnel.Route.Host + ".") && q.Qtype == dns.TypeA {
|
||||
if matchesDomain(q.Name, tunnel.Route.Host) && q.Qtype == dns.TypeA {
|
||||
utils.Debug("DNS Overwrite " + tunnel.Route.Host + " with " + destination)
|
||||
rr, _ := dns.NewRR(q.Name + " A " + destination)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
@@ -114,7 +128,7 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
for deviceName, ip := range CachedDeviceNames {
|
||||
procDeviceName := strings.ReplaceAll(deviceName, " ", "-")
|
||||
|
||||
if strings.HasSuffix(q.Name, procDeviceName + ".") && q.Qtype == dns.TypeA {
|
||||
if matchesDomain(q.Name, procDeviceName) && q.Qtype == dns.TypeA {
|
||||
utils.Debug("DNS Overwrite " + procDeviceName + " with its IP")
|
||||
rr, _ := dns.NewRR(q.Name + " A " + ip)
|
||||
m.Answer = append(m.Answer, rr)
|
||||
@@ -146,6 +160,8 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
externalResponse, time, err := externalLookup(client, r, DNSFallback)
|
||||
if err != nil {
|
||||
utils.Error("Failed to forward query:", err)
|
||||
m.SetRcode(r, dns.RcodeServerFailure)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
utils.Debug("DNS Forwarded DNS query to "+DNSFallback+" in " + time.String())
|
||||
@@ -166,18 +182,18 @@ func isDomain(domain string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func loadRawBlockList(DNSBlacklistRaw string) {
|
||||
func loadRawBlockList(blacklist map[string]bool, DNSBlacklistRaw string) {
|
||||
DNSBlacklistArray := strings.Split(string(DNSBlacklistRaw), "\n")
|
||||
for _, domain := range DNSBlacklistArray {
|
||||
if domain != "" && !strings.HasPrefix(domain, "#") {
|
||||
splitDomain := strings.Split(domain, " ")
|
||||
if len(splitDomain) == 1 && isDomain(splitDomain[0]) {
|
||||
DNSBlacklist[splitDomain[0]] = true
|
||||
blacklist[splitDomain[0]] = true
|
||||
} else if len(splitDomain) == 2 {
|
||||
if isDomain(splitDomain[0]) {
|
||||
DNSBlacklist[splitDomain[0]] = true
|
||||
blacklist[splitDomain[0]] = true
|
||||
} else if isDomain(splitDomain[1]) {
|
||||
DNSBlacklist[splitDomain[1]] = true
|
||||
blacklist[splitDomain[1]] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,11 +201,45 @@ func loadRawBlockList(DNSBlacklistRaw string) {
|
||||
}
|
||||
|
||||
var DNSStarted = false
|
||||
var dnsServer *dns.Server
|
||||
var dnsStarting = false
|
||||
var dnsMux sync.Mutex
|
||||
|
||||
// isCurrentDNSServer reports whether s is still the active server (false after StopDNS)
|
||||
func isCurrentDNSServer(s *dns.Server) bool {
|
||||
dnsMux.Lock()
|
||||
defer dnsMux.Unlock()
|
||||
return dnsServer == s
|
||||
}
|
||||
|
||||
func StopDNS() {
|
||||
dnsMux.Lock()
|
||||
server := dnsServer
|
||||
dnsServer = nil
|
||||
DNSStarted = false
|
||||
dnsMux.Unlock()
|
||||
|
||||
if server != nil {
|
||||
utils.Log("Stopping Constellation DNS")
|
||||
// bounded shutdown so a hung handler can never block stop()/RestartNebula
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := server.ShutdownContext(ctx); err != nil {
|
||||
// includes the benign "server not started" case when stopping mid-bind
|
||||
utils.Warn("Failed to stop DNS server: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InitDNS() {
|
||||
if DNSStarted {
|
||||
dnsMux.Lock()
|
||||
if dnsStarting || DNSStarted || dnsServer != nil {
|
||||
dnsMux.Unlock()
|
||||
return
|
||||
}
|
||||
// claim before the slow blacklist work so a concurrent InitDNS cannot double-start
|
||||
dnsStarting = true
|
||||
dnsMux.Unlock()
|
||||
|
||||
utils.Log("Initializing Constellation DNS setup")
|
||||
|
||||
@@ -203,7 +253,8 @@ func InitDNS() {
|
||||
}
|
||||
|
||||
if DNSBlockBlacklist {
|
||||
DNSBlacklist = map[string]bool{}
|
||||
// build into a local map and swap once, so live handlers never see a half-built list
|
||||
newBlacklist := map[string]bool{}
|
||||
blacklistPath := utils.CONFIGFOLDER + "dns-blacklist.txt"
|
||||
|
||||
utils.Log("Loading DNS blacklist from " + blacklistPath)
|
||||
@@ -214,7 +265,7 @@ func InitDNS() {
|
||||
if err != nil {
|
||||
utils.Error("Failed to load DNS blacklist", err)
|
||||
} else {
|
||||
loadRawBlockList(string(DNSBlacklistRaw))
|
||||
loadRawBlockList(newBlacklist, string(DNSBlacklistRaw))
|
||||
}
|
||||
} else {
|
||||
utils.Log("No DNS blacklist found")
|
||||
@@ -227,46 +278,77 @@ func InitDNS() {
|
||||
if err != nil {
|
||||
utils.Error("Failed to download DNS blacklist", err)
|
||||
} else {
|
||||
loadRawBlockList(DNSBlacklistRaw)
|
||||
loadRawBlockList(newBlacklist, DNSBlacklistRaw)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DNSBlacklist = newBlacklist
|
||||
|
||||
utils.Log("Loaded " + strconv.Itoa(len(DNSBlacklist)) + " domains")
|
||||
}
|
||||
|
||||
if(!config.ConstellationConfig.DNSDisabled) {
|
||||
utils.Log("Initializing Constellation DNS")
|
||||
if config.ConstellationConfig.DNSDisabled {
|
||||
dnsMux.Lock()
|
||||
dnsStarting = false
|
||||
dnsMux.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
go (func() {
|
||||
currIp, err := GetCurrentDeviceIP()
|
||||
if err != nil {
|
||||
utils.Error("Constellation DNS: Failed to get current device IP", err)
|
||||
utils.Log("Initializing Constellation DNS")
|
||||
|
||||
go (func() {
|
||||
currIp, err := GetCurrentDeviceIP()
|
||||
if err != nil {
|
||||
utils.Error("Constellation DNS: Failed to get current device IP", err)
|
||||
dnsMux.Lock()
|
||||
dnsStarting = false
|
||||
dnsMux.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
dns.HandleFunc(".", handleDNSRequest)
|
||||
server := &dns.Server{Addr: currIp + ":" + DNSPort, Net: "udp"}
|
||||
|
||||
// only report started once the socket is actually bound
|
||||
server.NotifyStartedFunc = func() {
|
||||
dnsMux.Lock()
|
||||
// a StopDNS raced the bind: shut this instance down instead of running as a zombie
|
||||
if dnsServer != server {
|
||||
dnsMux.Unlock()
|
||||
go server.Shutdown()
|
||||
return
|
||||
}
|
||||
|
||||
dns.HandleFunc(".", handleDNSRequest)
|
||||
server := &dns.Server{Addr: currIp + ":" + DNSPort, Net: "udp"}
|
||||
|
||||
utils.Log("Starting DNS server on :" + DNSPort)
|
||||
|
||||
DNSStarted = true
|
||||
dnsMux.Unlock()
|
||||
utils.Log("Constellation DNS started!")
|
||||
}
|
||||
|
||||
dnsMux.Lock()
|
||||
dnsServer = server
|
||||
dnsStarting = false
|
||||
dnsMux.Unlock()
|
||||
|
||||
utils.Log("Starting DNS server on :" + DNSPort)
|
||||
|
||||
err = server.ListenAndServe();
|
||||
retries := 0
|
||||
|
||||
for err != nil && retries < 4 && isCurrentDNSServer(server) {
|
||||
time.Sleep(time.Duration(2 * (retries + 1)) * time.Second)
|
||||
err = server.ListenAndServe();
|
||||
retries := 0
|
||||
retries++
|
||||
utils.Debug("Retrying to start DNS server")
|
||||
}
|
||||
|
||||
for err != nil && retries < 4 {
|
||||
time.Sleep(time.Duration(2 * (retries + 1)) * time.Second)
|
||||
err = server.ListenAndServe();
|
||||
retries++
|
||||
utils.Debug("Retrying to start DNS server")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
utils.MajorError("Failed to start DNS server", err)
|
||||
DNSStarted = false
|
||||
} else {
|
||||
utils.Log("Constellation DNS started!")
|
||||
}
|
||||
})()
|
||||
}
|
||||
if err != nil && isCurrentDNSServer(server) {
|
||||
utils.MajorError("Failed to start DNS server", err)
|
||||
}
|
||||
|
||||
dnsMux.Lock()
|
||||
if dnsServer == server {
|
||||
dnsServer = nil
|
||||
DNSStarted = false
|
||||
}
|
||||
dnsMux.Unlock()
|
||||
})()
|
||||
}
|
||||
|
||||
+78
-56
@@ -64,6 +64,17 @@ func truncateLog(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// redactSecret returns a short, non-reversible hint of a secret for logs
|
||||
func redactSecret(s string) string {
|
||||
if s == "" {
|
||||
return "<empty>"
|
||||
}
|
||||
if len(s) <= 4 {
|
||||
return "****(len " + strconv.Itoa(len(s)) + ")"
|
||||
}
|
||||
return s[:4] + "****(len " + strconv.Itoa(len(s)) + ")"
|
||||
}
|
||||
|
||||
func sanitizeNATSUsername(username string) string {
|
||||
username = strings.ReplaceAll(username, " ", "_")
|
||||
username = strings.ReplaceAll(username, ".", "_")
|
||||
@@ -109,7 +120,7 @@ func GetClusterIPs() ([]*url.URL, error) {
|
||||
func GetNATSCredentials() (string, string, error) {
|
||||
currentDevice, _ := GetCurrentDevice()
|
||||
|
||||
utils.Debug("GetNATSCredentials: currentDevice.APIKey=" + currentDevice.APIKey + " currentDevice.DeviceName=" + currentDevice.DeviceName)
|
||||
utils.Debug("GetNATSCredentials: currentDevice.APIKey=" + redactSecret(currentDevice.APIKey) + " currentDevice.DeviceName=" + currentDevice.DeviceName)
|
||||
|
||||
if currentDevice.APIKey != "" && currentDevice.DeviceName != "" {
|
||||
return currentDevice.DeviceName, currentDevice.APIKey, nil
|
||||
@@ -135,12 +146,10 @@ func GetNATSCredentials() (string, string, error) {
|
||||
apiKey := configMap["cstln_api_key"].(string)
|
||||
deviceName := configMap["cstln_device_name"].(string)
|
||||
|
||||
utils.Debug("GetNATSCredentials: found credentials in nebula.yml: deviceName=" + deviceName + " apiKey=" + apiKey)
|
||||
utils.Debug("GetNATSCredentials: found credentials in nebula.yml: deviceName=" + deviceName + " apiKey=" + redactSecret(apiKey))
|
||||
|
||||
return deviceName, apiKey, nil
|
||||
}
|
||||
|
||||
return "", "", errors.New("NATS credentials not found")
|
||||
}
|
||||
|
||||
func StartNATS() {
|
||||
@@ -172,18 +181,21 @@ func StartNATS() {
|
||||
certDERBlock, _ := pem.Decode(certPEMBlock)
|
||||
if certDERBlock == nil {
|
||||
utils.MajorError("[NATS] Failed to start NATS: parse certificate PEM", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode PEM encoded private key
|
||||
keyDERBlock, _ := pem.Decode(keyPEMBlock)
|
||||
if keyDERBlock == nil {
|
||||
utils.MajorError("[NATS] Failed to start NATS: parse key PEM", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Create tls.Certificate using the original PEM data
|
||||
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
||||
if err != nil {
|
||||
utils.MajorError("[NATS] Failed to start NATS: create TLS certificate", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Configure the NATS server options
|
||||
@@ -192,7 +204,7 @@ func StartNATS() {
|
||||
users := []*server.User{}
|
||||
|
||||
for _, devices := range CachedDevices {
|
||||
utils.Debug("[NATS] Adding NATS user for device: " + devices.DeviceName + " With API Key: " + devices.APIKey)
|
||||
utils.Debug("[NATS] Adding NATS user for device: " + devices.DeviceName + " With API Key: " + redactSecret(devices.APIKey))
|
||||
username := sanitizeNATSUsername(devices.DeviceName)
|
||||
|
||||
// TODO: Agent / users with less permissions
|
||||
@@ -251,8 +263,10 @@ func StartNATS() {
|
||||
natsHost = "0.0.0.0"
|
||||
}
|
||||
|
||||
standalone := IsConstellationStandalone()
|
||||
|
||||
cips, err := GetClusterIPs()
|
||||
if err != nil {
|
||||
if err != nil && !standalone {
|
||||
utils.Error("[NATS] Failed to get cluster IPs", err)
|
||||
}
|
||||
|
||||
@@ -261,13 +275,6 @@ func StartNATS() {
|
||||
utils.Debug("[NATS] Cluster IP: " + cip.String())
|
||||
}
|
||||
|
||||
// Abort if this server has no peer lighthouses — a single-node NATS cluster
|
||||
// cannot form a JetStream quorum and every downstream op spams errors.
|
||||
if IsConstellationStandalone() {
|
||||
utils.Warn("[NATS] Constellation has no peer lighthouses, NATS server will not start")
|
||||
return
|
||||
}
|
||||
|
||||
opts := &server.Options{
|
||||
Host: natsHost,
|
||||
Port: 4222,
|
||||
@@ -283,7 +290,14 @@ func StartNATS() {
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
|
||||
Cluster: server.ClusterOpts{
|
||||
Users: users,
|
||||
}
|
||||
|
||||
// no peer lighthouses: serve NATS single-node, without cluster opts or routes
|
||||
if standalone {
|
||||
utils.Log("[NATS] Constellation has no peer lighthouses, starting NATS in standalone mode")
|
||||
} else {
|
||||
opts.Cluster = server.ClusterOpts{
|
||||
Name: "Constellation",
|
||||
Host: device.IP,
|
||||
Port: 6222,
|
||||
@@ -292,11 +306,9 @@ func StartNATS() {
|
||||
ClientAuth: tls.NoClientCert,
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Routes: cips,
|
||||
|
||||
Users: users,
|
||||
opts.Routes = cips
|
||||
}
|
||||
|
||||
// Create and start the embedded NATS server
|
||||
@@ -304,6 +316,13 @@ func StartNATS() {
|
||||
err = errors.New("")
|
||||
|
||||
for err != nil && retries < 5 {
|
||||
// drop any instance left over from a failed attempt
|
||||
if ns != nil {
|
||||
ns.Shutdown()
|
||||
ns.WaitForShutdown()
|
||||
ns = nil
|
||||
}
|
||||
|
||||
ns, err = server.NewServer(opts)
|
||||
if err != nil {
|
||||
retries++
|
||||
@@ -312,6 +331,8 @@ func StartNATS() {
|
||||
|
||||
if !NebulaStarted {
|
||||
utils.Error("[NATS] Nebula not started, aborting NATS server setup", nil)
|
||||
ns.Shutdown()
|
||||
ns = nil
|
||||
return
|
||||
}
|
||||
|
||||
@@ -328,14 +349,20 @@ func StartNATS() {
|
||||
utils.Debug("[NATS] Retrying to start NATS server")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if ns != nil {
|
||||
ns.Shutdown()
|
||||
ns.WaitForShutdown()
|
||||
ns = nil
|
||||
}
|
||||
utils.MajorError("[NATS] Error starting NATS server", err)
|
||||
return
|
||||
}
|
||||
|
||||
NATSStarted = true
|
||||
|
||||
if err != nil {
|
||||
utils.MajorError("[NATS] Error starting NATS server", err)
|
||||
} else {
|
||||
utils.Log("[NATS] Started NATS server on host " + opts.Host + ":" + strconv.Itoa(opts.Port))
|
||||
InitNATSClient()
|
||||
}
|
||||
utils.Log("[NATS] Started NATS server on host " + opts.Host + ":" + strconv.Itoa(opts.Port))
|
||||
InitNATSClient()
|
||||
}
|
||||
|
||||
func StopNATS() {
|
||||
@@ -355,6 +382,21 @@ var NATSClientTopic = ""
|
||||
var nc *nats.Conn
|
||||
var js nats.JetStreamContext
|
||||
|
||||
func connectNATSClient(url string, user string, pwd string) (*nats.Conn, error) {
|
||||
return natsClient.Connect(url,
|
||||
nats.Secure(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}),
|
||||
|
||||
nats.UserInfo(user, pwd),
|
||||
|
||||
// timeout
|
||||
nats.Timeout(2*time.Second),
|
||||
|
||||
nats.NoEcho(),
|
||||
)
|
||||
}
|
||||
|
||||
func InitNATSClient() error {
|
||||
if !NATSStarted {
|
||||
utils.Warn("[NATS] NATS server not started, cannot initialize client")
|
||||
@@ -394,23 +436,9 @@ func InitNATSClient() error {
|
||||
return err
|
||||
}
|
||||
|
||||
nc, err = natsClient.Connect("nats://"+deviceIp+":4222",
|
||||
natsUrl := "nats://" + deviceIp + ":4222"
|
||||
|
||||
// nats.DisconnectHandler(func(nc *nats.Conn) {
|
||||
// utils.Log("Disconnected from NATS server - trying to reconnect")
|
||||
// }),
|
||||
|
||||
nats.Secure(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}),
|
||||
|
||||
nats.UserInfo(user, pwd),
|
||||
|
||||
// timeout
|
||||
nats.Timeout(2*time.Second),
|
||||
|
||||
nats.NoEcho(),
|
||||
)
|
||||
nc, err = connectNATSClient(natsUrl, user, pwd)
|
||||
|
||||
for err != nil {
|
||||
if retries >= 10 {
|
||||
@@ -435,18 +463,7 @@ func InitNATSClient() error {
|
||||
continue
|
||||
}
|
||||
|
||||
nc, err = natsClient.Connect("nats://localhost:4222",
|
||||
nats.Secure(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}),
|
||||
|
||||
nats.UserInfo(user, pwd),
|
||||
|
||||
// timeout
|
||||
nats.Timeout(2*time.Second),
|
||||
|
||||
nats.NoEcho(),
|
||||
)
|
||||
nc, err = connectNATSClient(natsUrl, user, pwd)
|
||||
|
||||
if err != nil {
|
||||
retries++
|
||||
@@ -473,9 +490,14 @@ func InitNATSClient() error {
|
||||
utils.Error("[NATS] Failed to get JetStream context", err)
|
||||
}
|
||||
|
||||
go ClientHeartbeatInit()
|
||||
// standalone has no peers to heartbeat with or sync from
|
||||
if IsConstellationStandalone() {
|
||||
utils.Debug("[NATS] Standalone constellation, skipping heartbeat and sync request")
|
||||
} else {
|
||||
go ClientHeartbeatInit()
|
||||
|
||||
go SendRequestSyncMessage()
|
||||
go SendRequestSyncMessage()
|
||||
}
|
||||
|
||||
// POST CLIENT CONNECTION HOOK
|
||||
|
||||
@@ -535,8 +557,8 @@ func IsConstellationStandalone() bool {
|
||||
}
|
||||
myIP, err := GetCurrentDeviceIP()
|
||||
if err != nil {
|
||||
utils.Fatal("[NATS] Failed to get current device IP", err)
|
||||
return false
|
||||
utils.Error("[NATS] Failed to get current device IP", err)
|
||||
return true
|
||||
}
|
||||
for _, device := range CachedDevices {
|
||||
if device.IP == myIP {
|
||||
|
||||
@@ -51,18 +51,13 @@ func DeviceBlock(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
nickname := utils.Sanitize(request.Nickname)
|
||||
deviceName := utils.Sanitize(request.DeviceName)
|
||||
|
||||
if utils.CheckPermissionsOrSelf(w, req, nickname, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Log("ConstellationDeviceBlocking: Blocking Device " + deviceName)
|
||||
|
||||
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
|
||||
defer closeDb()
|
||||
|
||||
|
||||
if errCo != nil {
|
||||
utils.Error("Database Connect", errCo)
|
||||
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||
@@ -72,7 +67,7 @@ func DeviceBlock(w http.ResponseWriter, req *http.Request) {
|
||||
device := utils.Device{}
|
||||
|
||||
utils.Debug("ConstellationDeviceBlocking: Blocking Device " + deviceName)
|
||||
|
||||
|
||||
err2 := c.FindOne(nil, map[string]interface{}{
|
||||
"DeviceName": deviceName,
|
||||
"Blocked": false,
|
||||
@@ -80,7 +75,12 @@ func DeviceBlock(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if err2 == nil {
|
||||
utils.Debug("ConstellationDeviceBlocking: Found Device " + deviceName)
|
||||
|
||||
|
||||
// authorize against the device's actual nickname, not the request-supplied one
|
||||
if utils.CheckPermissionsOrSelf(w, req, device.Nickname, utils.PERM_RESOURCES) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err3 := c.UpdateMany(nil, map[string]interface{}{
|
||||
"DeviceName": deviceName,
|
||||
}, map[string]interface{}{
|
||||
|
||||
@@ -5,15 +5,18 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// deviceCreateMutex serializes DeviceCreate to close the name/IP check-then-insert race
|
||||
var deviceCreateMutex sync.Mutex
|
||||
|
||||
type DeviceCreateRequestJSON struct {
|
||||
DeviceName string `json:"deviceName" validate:"required,min=3,max=32"`
|
||||
IP string `json:"ip" validate:"required,ipv4"`
|
||||
PublicKey string `json:"publicKey,omitempty"`
|
||||
|
||||
// for devices only
|
||||
Nickname string `json:"nickname,omitempty" validate:"omitempty,max=32"`
|
||||
@@ -75,11 +78,15 @@ func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// Non-admin users can only create client devices
|
||||
if !utils.HasPermission(req, utils.PERM_RESOURCES) && request.IsLighthouse {
|
||||
utils.Error("DeviceCreation: Non-admin users can only create client devices", nil)
|
||||
utils.HTTPError(w, "Device Creation Error: Only administrators can create lighthouse devices",
|
||||
http.StatusForbidden, "DC006")
|
||||
return
|
||||
if !utils.HasPermission(req, utils.PERM_RESOURCES) {
|
||||
if request.IsLighthouse {
|
||||
utils.Error("DeviceCreation: Non-admin users can only create client devices", nil)
|
||||
utils.HTTPError(w, "Device Creation Error: Only administrators can create lighthouse devices",
|
||||
http.StatusForbidden, "DC006")
|
||||
return
|
||||
}
|
||||
|
||||
request.CosmosNode = 0
|
||||
}
|
||||
|
||||
errV := utils.Validate.Struct(request)
|
||||
@@ -114,7 +121,6 @@ func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
|
||||
configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml", capki, cert, key, APIKey, utils.ConstellationDevice{
|
||||
Nickname: nickname,
|
||||
DeviceName: deviceName,
|
||||
PublicKey: key,
|
||||
IP: request.IP,
|
||||
IsLighthouse: request.IsLighthouse,
|
||||
CosmosNode: request.CosmosNode,
|
||||
@@ -152,7 +158,6 @@ func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
|
||||
map[string]interface{}{
|
||||
"deviceName": deviceName,
|
||||
"nickname": nickname,
|
||||
"publicKey": key,
|
||||
"ip": request.IP,
|
||||
})
|
||||
|
||||
@@ -161,8 +166,8 @@ func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
|
||||
"data": map[string]interface{}{
|
||||
"Nickname": nickname,
|
||||
"DeviceName": deviceName,
|
||||
"PublicKey": key,
|
||||
"PrivateKey": cert,
|
||||
"Certificate": cert,
|
||||
"PrivateKey": key,
|
||||
"IP": request.IP,
|
||||
"Config": configYml,
|
||||
"CA": capki,
|
||||
@@ -197,6 +202,9 @@ func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func DeviceCreate(request DeviceCreateRequestJSON) (string, string, string, DeviceCreateRequestJSON, error) {
|
||||
deviceCreateMutex.Lock()
|
||||
defer deviceCreateMutex.Unlock()
|
||||
|
||||
nickname := utils.Sanitize(request.Nickname)
|
||||
deviceName := utils.Sanitize(request.DeviceName)
|
||||
APIKey := utils.GenerateRandomString(32)
|
||||
@@ -240,7 +248,7 @@ func DeviceCreate(request DeviceCreateRequestJSON) (string, string, string, Devi
|
||||
|
||||
// Device name and IP are both available, proceed with creation
|
||||
{
|
||||
cert, key, fingerprint, err := generateNebulaCert(deviceName, deviceName, request.IP, request.PublicKey, false)
|
||||
cert, key, fingerprint, err := generateNebulaCert(deviceName, deviceName, request.IP, false)
|
||||
|
||||
if err != nil {
|
||||
return "", "", "", DeviceCreateRequestJSON{}, err
|
||||
@@ -316,7 +324,6 @@ func DeviceCreate(request DeviceCreateRequestJSON) (string, string, string, Devi
|
||||
_, err3 := c.InsertOne(nil, map[string]interface{}{
|
||||
"Nickname": nickname,
|
||||
"DeviceName": deviceName,
|
||||
"PublicKey": key,
|
||||
"IP": request.IP,
|
||||
"IsLighthouse": request.IsLighthouse,
|
||||
"CosmosNode": request.CosmosNode,
|
||||
@@ -337,11 +344,6 @@ func DeviceCreate(request DeviceCreateRequestJSON) (string, string, string, Devi
|
||||
|
||||
request.Nickname = nickname
|
||||
request.DeviceName = deviceName
|
||||
request.PublicKey = key
|
||||
request.IsLoadBalancer = request.IsLoadBalancer
|
||||
request.IsExitNode = request.IsExitNode
|
||||
request.IsLoadBalancer = request.IsLoadBalancer
|
||||
request.Invisible = request.Invisible
|
||||
request.APIKey = APIKey
|
||||
|
||||
return cert, key, fingerprint, request, nil
|
||||
|
||||
@@ -44,13 +44,13 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
|
||||
if isAdmin {
|
||||
// If admin, get all devices
|
||||
cursor, err := c.Find(nil, map[string]interface{}{})
|
||||
defer cursor.Close(nil)
|
||||
if err != nil {
|
||||
utils.Error("DeviceList: Error fetching devices", err)
|
||||
utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DL001")
|
||||
return
|
||||
}
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err = cursor.All(nil, &devices); err != nil {
|
||||
utils.Error("DeviceList: Error decoding devices", err)
|
||||
utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DL002")
|
||||
@@ -60,13 +60,13 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
|
||||
// If not admin, get user's devices based on their nickname
|
||||
nickname := utils.GetAuthContext(req).Nickname
|
||||
cursor, err := c.Find(nil, map[string]interface{}{"Nickname": nickname})
|
||||
defer cursor.Close(nil)
|
||||
if err != nil {
|
||||
utils.Error("DeviceList: Error fetching devices", err)
|
||||
utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DL003")
|
||||
return
|
||||
}
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err = cursor.All(nil, &devices); err != nil {
|
||||
utils.Error("DeviceList: Error decoding devices", err)
|
||||
utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DL004")
|
||||
|
||||
@@ -99,10 +99,7 @@ func DevicePing(w http.ResponseWriter, req *http.Request) {
|
||||
func pingIP(ip string) bool {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// remove the CIDR suffix if present
|
||||
if len(ip) > 3 && ip[len(ip)-3] == '/' {
|
||||
ip = ip[:len(ip)-3]
|
||||
}
|
||||
ip = cleanIp(ip)
|
||||
|
||||
// Use platform-specific ping command
|
||||
if runtime.GOOS == "windows" {
|
||||
|
||||
@@ -60,13 +60,12 @@ func DevicePublicList(w http.ResponseWriter, req *http.Request) {
|
||||
"Invisible": false,
|
||||
})
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err != nil {
|
||||
utils.Error("DevicePublicList: Error fetching devices", err)
|
||||
utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DPL001")
|
||||
return
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
|
||||
var devices []utils.ConstellationDevice
|
||||
if err = cursor.All(nil, &devices); err != nil {
|
||||
@@ -117,13 +116,12 @@ func PublicDeviceListNATS(m *nats.Msg) {
|
||||
"Invisible": false,
|
||||
})
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err != nil {
|
||||
utils.Error("PublicDeviceListNATS: Error fetching devices", err)
|
||||
m.Respond([]byte(`{"status":"error","message":"Error fetching devices"}`))
|
||||
return
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
|
||||
var devices []utils.ConstellationDevice
|
||||
if err = cursor.All(nil, &devices); err != nil {
|
||||
|
||||
@@ -41,11 +41,11 @@ func CheckConstellationToken(req *http.Request) error {
|
||||
"APIKey": auth,
|
||||
"Blocked": false,
|
||||
})
|
||||
defer cursor.Close(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
// if any device is found, return config without keys
|
||||
if cursor.Next(nil) {
|
||||
return nil
|
||||
@@ -163,7 +163,8 @@ func API_GetLogs(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "GET") {
|
||||
logs, err := os.ReadFile(utils.CONFIGFOLDER+"nebula.log")
|
||||
if err != nil {
|
||||
utils.Error("Error reading file:", err)
|
||||
utils.Error("API_GetLogs: Error reading nebula.log", err)
|
||||
utils.HTTPError(w, "Error reading logs", http.StatusInternalServerError, "AGL001")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ func API_NewConstellation(w http.ResponseWriter, req *http.Request) {
|
||||
utils.Log("Constellation: cosmos.crt generating with ip " + ip)
|
||||
|
||||
// generate cosmos.crt
|
||||
_,_,_,errG = generateNebulaCert(request.DeviceName, "cosmos", ip, "", true)
|
||||
_,_,_,errG = generateNebulaCert(request.DeviceName, "cosmos", ip, true)
|
||||
if errG != nil {
|
||||
utils.Error("Constellation: error while generating cosmos.crt", errG)
|
||||
}
|
||||
@@ -188,25 +188,45 @@ func ConnectToExisting(yamlBody []byte, config utils.Config) (utils.Config, erro
|
||||
}
|
||||
|
||||
if deviceNameVal, ok := configMap["cstln_device_name"]; ok {
|
||||
config.ConstellationConfig.ThisDeviceName = deviceNameVal.(string)
|
||||
deviceName, ok := deviceNameVal.(string)
|
||||
if !ok {
|
||||
return config, errors.New("cstln_device_name is not a string")
|
||||
}
|
||||
config.ConstellationConfig.ThisDeviceName = deviceName
|
||||
} else {
|
||||
return config, errors.New("device name not found in constellation config")
|
||||
}
|
||||
|
||||
if publicHostnameVal, ok := configMap["cstln_public_hostname"]; ok {
|
||||
config.ConstellationConfig.ConstellationHostname = publicHostnameVal.(string)
|
||||
publicHostname, ok := publicHostnameVal.(string)
|
||||
if !ok {
|
||||
return config, errors.New("cstln_public_hostname is not a string")
|
||||
}
|
||||
config.ConstellationConfig.ConstellationHostname = publicHostname
|
||||
}
|
||||
|
||||
if licence, ok := configMap["cstln_server_licence"]; ok {
|
||||
config.Licence = licence.(string)
|
||||
if licenceVal, ok := configMap["cstln_server_licence"]; ok {
|
||||
licence, ok := licenceVal.(string)
|
||||
if !ok {
|
||||
return config, errors.New("cstln_server_licence is not a string")
|
||||
}
|
||||
config.Licence = licence
|
||||
}
|
||||
|
||||
if cosmosNode, ok := configMap["cstln_cosmos_node"]; ok {
|
||||
config.AgentMode = cosmosNode.(int) == 1
|
||||
if cosmosNodeVal, ok := configMap["cstln_cosmos_node"]; ok {
|
||||
cosmosNode, ok := cosmosNodeVal.(int)
|
||||
if !ok {
|
||||
return config, errors.New("cstln_cosmos_node is not an int")
|
||||
}
|
||||
config.AgentMode = cosmosNode == 1
|
||||
}
|
||||
|
||||
if ipRange, ok := configMap["cstln_ip_range"]; ok {
|
||||
config.ConstellationConfig.IPRange = ipRange.(string)
|
||||
if ipRangeVal, ok := configMap["cstln_ip_range"]; ok {
|
||||
ipRange, ok := ipRangeVal.(string)
|
||||
if !ok {
|
||||
return config, errors.New("cstln_ip_range is not a string")
|
||||
}
|
||||
config.ConstellationConfig.IPRange = ipRange
|
||||
}
|
||||
|
||||
utils.Log("ConnectToExisting: connected to an external Constellation")
|
||||
|
||||
@@ -113,7 +113,9 @@ func UpdateFirewallBlockedClients() error {
|
||||
return err
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
cursor.All(nil, &devices)
|
||||
if err := cursor.All(nil, &devices); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a map of device names to IPs
|
||||
deviceIPs := make(map[string]string)
|
||||
@@ -477,8 +479,7 @@ func ValidateStaticHosts(logBuffer *lumberjack.Logger) error {
|
||||
lighthouseMap["hosts"] = newHosts
|
||||
removedCount := originalCount - len(newHosts)
|
||||
if removedCount > 0 {
|
||||
logger.Printf("level=debug msg=Removed %d completely unreachable IP(s) from relay list", removedCount)
|
||||
logger.Printf("Removed %d completely unreachable IP(s) from lighthouse hosts", removedCount)
|
||||
logger.Printf("level=debug msg=Removed %d completely unreachable IP(s) from lighthouse hosts", removedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package constellation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
func TestUnitMatchesDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
qName string
|
||||
hostname string
|
||||
want bool
|
||||
}{
|
||||
{"exact match", "myhost.com.", "myhost.com", true},
|
||||
{"subdomain match", "a.myhost.com.", "myhost.com", true},
|
||||
{"deep subdomain match", "a.b.myhost.com.", "myhost.com", true},
|
||||
{"regression: suffix without label boundary", "evilmyhost.com.", "myhost.com", false},
|
||||
{"non-match", "other.com.", "myhost.com", false},
|
||||
{"missing trailing dot", "myhost.com", "myhost.com", false},
|
||||
{"empty qName", "", "myhost.com", false},
|
||||
{"empty hostname", "myhost.com.", "", false},
|
||||
{"both empty", "", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := matchesDomain(tt.qName, tt.hostname); got != tt.want {
|
||||
t.Errorf("matchesDomain(%q, %q) = %v, want %v", tt.qName, tt.hostname, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitIsDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
domain string
|
||||
want bool
|
||||
}{
|
||||
{"ads.example.com", true},
|
||||
{"example.com", true},
|
||||
{"0.0.0.0", false},
|
||||
{"localhost", false},
|
||||
{"", false},
|
||||
{"bad domain.com", false},
|
||||
{"weird!.com", false},
|
||||
{"path/to.com", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := isDomain(tt.domain); got != tt.want {
|
||||
t.Errorf("isDomain(%q) = %v, want %v", tt.domain, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitLoadRawBlockList(t *testing.T) {
|
||||
raw := strings.Join([]string{
|
||||
"# comment line",
|
||||
"",
|
||||
"0.0.0.0 ads.example.com",
|
||||
"bare-domain.org",
|
||||
"garbage line with words",
|
||||
"tracker.net 0.0.0.0",
|
||||
"0.0.0.0",
|
||||
"# another.commented.com",
|
||||
}, "\n")
|
||||
|
||||
blacklist := map[string]bool{}
|
||||
loadRawBlockList(blacklist, raw)
|
||||
|
||||
want := map[string]bool{
|
||||
"ads.example.com": true,
|
||||
"bare-domain.org": true,
|
||||
"tracker.net": true,
|
||||
}
|
||||
if len(blacklist) != len(want) {
|
||||
t.Errorf("blacklist has %d entries, want %d: %v", len(blacklist), len(want), blacklist)
|
||||
}
|
||||
for domain := range want {
|
||||
if !blacklist[domain] {
|
||||
t.Errorf("expected %q in blacklist", domain)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"0.0.0.0", "# comment line", "garbage", "another.commented.com"} {
|
||||
if blacklist[bad] {
|
||||
t.Errorf("unexpected %q in blacklist", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitSanitizeNATSUsername(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"my device", "my_device"},
|
||||
{"a.b-c:d/e\\f", "a_b_c_d_e_f"},
|
||||
{"clean_name", "clean_name"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := sanitizeNATSUsername(tt.in); got != tt.want {
|
||||
t.Errorf("sanitizeNATSUsername(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitRedactSecret(t *testing.T) {
|
||||
if got := redactSecret(""); got != "<empty>" {
|
||||
t.Errorf("redactSecret(\"\") = %q, want \"<empty>\"", got)
|
||||
}
|
||||
|
||||
// short secrets must not leak any prefix
|
||||
short := "abcd"
|
||||
got := redactSecret(short)
|
||||
if got != "****(len 4)" {
|
||||
t.Errorf("redactSecret(%q) = %q, want \"****(len 4)\"", short, got)
|
||||
}
|
||||
if strings.Contains(got, "a") {
|
||||
t.Errorf("short secret prefix leaked in %q", got)
|
||||
}
|
||||
|
||||
// long secrets show a 4-char prefix and length only
|
||||
long := "supersecretvalue"
|
||||
got = redactSecret(long)
|
||||
if got != "supe****(len 16)" {
|
||||
t.Errorf("redactSecret(%q) = %q, want \"supe****(len 16)\"", long, got)
|
||||
}
|
||||
if strings.Contains(got, long[4:]) {
|
||||
t.Errorf("secret suffix leaked in %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitTruncateLog(t *testing.T) {
|
||||
short := "hello"
|
||||
if got := truncateLog(short); got != short {
|
||||
t.Errorf("truncateLog(%q) = %q, want unchanged", short, got)
|
||||
}
|
||||
|
||||
exact := strings.Repeat("x", 100)
|
||||
if got := truncateLog(exact); got != exact {
|
||||
t.Errorf("truncateLog of 100 chars should be unchanged, got %q", got)
|
||||
}
|
||||
|
||||
long := strings.Repeat("y", 150)
|
||||
got := truncateLog(long)
|
||||
if got != strings.Repeat("y", 100)+"..." {
|
||||
t.Errorf("truncateLog of 150 chars = %q, want 100 chars + ellipsis", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitCleanIp(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"192.168.201.1/24", "192.168.201.1"},
|
||||
{"192.168.201.1", "192.168.201.1"},
|
||||
{"10.0.0.5/16", "10.0.0.5"},
|
||||
{"", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := cleanIp(tt.in); got != tt.want {
|
||||
t.Errorf("cleanIp(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitGetCertPrefixLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ipRange string
|
||||
want string
|
||||
}{
|
||||
{"empty defaults to 24", "", "24"},
|
||||
{"valid /16 range", "192.168.0.0/16", "16"},
|
||||
{"invalid range defaults to 24", "not-a-cidr", "24"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setupTestEnv(t, func(cfg *utils.Config) {
|
||||
cfg.ConstellationConfig.IPRange = tt.ipRange
|
||||
})
|
||||
if got := getCertPrefixLength(); got != tt.want {
|
||||
t.Errorf("getCertPrefixLength() with IPRange %q = %q, want %q", tt.ipRange, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitConnectToExistingValid(t *testing.T) {
|
||||
setupTestEnv(t, nil)
|
||||
|
||||
yamlBody := []byte(`
|
||||
cstln_device_name: my-device
|
||||
cstln_public_hostname: vpn.example.com
|
||||
cstln_ip_range: 10.10.0.0/16
|
||||
cstln_cosmos_node: 1
|
||||
`)
|
||||
cfg, err := ConnectToExisting(yamlBody, utils.GetMainConfig())
|
||||
if err != nil {
|
||||
t.Fatal("ConnectToExisting:", err)
|
||||
}
|
||||
if cfg.ConstellationConfig.ThisDeviceName != "my-device" {
|
||||
t.Errorf("ThisDeviceName = %q, want \"my-device\"", cfg.ConstellationConfig.ThisDeviceName)
|
||||
}
|
||||
if cfg.ConstellationConfig.ConstellationHostname != "vpn.example.com" {
|
||||
t.Errorf("ConstellationHostname = %q, want \"vpn.example.com\"", cfg.ConstellationConfig.ConstellationHostname)
|
||||
}
|
||||
if cfg.ConstellationConfig.IPRange != "10.10.0.0/16" {
|
||||
t.Errorf("IPRange = %q, want \"10.10.0.0/16\"", cfg.ConstellationConfig.IPRange)
|
||||
}
|
||||
if !cfg.AgentMode {
|
||||
t.Error("AgentMode = false, want true (cstln_cosmos_node: 1)")
|
||||
}
|
||||
if !cfg.ConstellationConfig.Enabled {
|
||||
t.Error("Enabled = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitConnectToExistingWrongTypes(t *testing.T) {
|
||||
// regression: wrong-typed fields must return an error, not panic
|
||||
tests := []struct {
|
||||
name string
|
||||
yaml string
|
||||
}{
|
||||
{"device name is int", "cstln_device_name: 123"},
|
||||
{"public hostname is int", "cstln_device_name: ok\ncstln_public_hostname: 42"},
|
||||
{"licence is int", "cstln_device_name: ok\ncstln_server_licence: 42"},
|
||||
{"cosmos node is string", "cstln_device_name: ok\ncstln_cosmos_node: notanint"},
|
||||
{"ip range is int", "cstln_device_name: ok\ncstln_ip_range: 99"},
|
||||
{"missing device name", "cstln_public_hostname: vpn.example.com"},
|
||||
{"invalid yaml", ":\n - ["},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setupTestEnv(t, nil)
|
||||
if _, err := ConnectToExisting([]byte(tt.yaml), utils.GetMainConfig()); err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -115,11 +115,11 @@ func Init() {
|
||||
utils.Error("Database Connect", errCo)
|
||||
} else {
|
||||
cursor, err := c.Find(nil, map[string]interface{}{})
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err != nil {
|
||||
utils.Error("DeviceList: Error fetching devices", err)
|
||||
} else {
|
||||
defer cursor.Close(nil)
|
||||
var devices []utils.ConstellationDevice
|
||||
|
||||
if err = cursor.All(nil, &devices); err != nil {
|
||||
|
||||
+141
-90
@@ -12,6 +12,7 @@ import (
|
||||
"gopkg.in/yaml.v2"
|
||||
"strings"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strconv"
|
||||
"encoding/json"
|
||||
"io"
|
||||
@@ -24,8 +25,9 @@ import (
|
||||
var logBuffer *lumberjack.Logger
|
||||
|
||||
var (
|
||||
process *exec.Cmd
|
||||
ProcessMux sync.Mutex
|
||||
process *exec.Cmd
|
||||
processDone chan struct{}
|
||||
ProcessMux sync.Mutex
|
||||
)
|
||||
|
||||
func binaryToRun() string {
|
||||
@@ -79,7 +81,9 @@ func startNebula() error {
|
||||
Compress: false,
|
||||
}
|
||||
|
||||
UpdateFirewallBlockedClients()
|
||||
if eFw := UpdateFirewallBlockedClients(); eFw != nil {
|
||||
utils.Error("Failed to update firewall blocked clients", eFw)
|
||||
}
|
||||
e := ExportLighthouseFromDB()
|
||||
AdjustDNS(logBuffer)
|
||||
if e != nil {
|
||||
@@ -121,7 +125,8 @@ func startNebula() error {
|
||||
NebulaHasStarted = true
|
||||
|
||||
// Monitor process
|
||||
go monitorNebulaProcess(process)
|
||||
processDone = make(chan struct{})
|
||||
go monitorNebulaProcess(process, processDone)
|
||||
|
||||
// Save PID
|
||||
if err := savePID(process.Process.Pid); err != nil {
|
||||
@@ -159,6 +164,20 @@ func handleProcessOutput(stdout, stderr io.ReadCloser, logBuffer *lumberjack.Log
|
||||
}()
|
||||
}
|
||||
|
||||
// isNebulaProcess checks the process identity to avoid killing an unrelated PID after PID reuse
|
||||
func isNebulaProcess(pid int) bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return true
|
||||
}
|
||||
|
||||
cmdline, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(strings.ToLower(string(cmdline)), "nebula")
|
||||
}
|
||||
|
||||
func killExistingProcess(pidFile string) error {
|
||||
pidBytes, err := ioutil.ReadFile(pidFile)
|
||||
if err != nil {
|
||||
@@ -170,6 +189,16 @@ func killExistingProcess(pidFile string) error {
|
||||
return fmt.Errorf("invalid pid format: %w", err)
|
||||
}
|
||||
|
||||
if !isNebulaProcess(pidInt) {
|
||||
utils.Warn(fmt.Sprintf("Constellation: PID %d is not a nebula process, removing stale PID file", pidInt))
|
||||
|
||||
if err := os.Remove(pidFile); err != nil {
|
||||
utils.Error("Failed to remove stale PID file", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proc, err := os.FindProcess(pidInt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding process: %w", err)
|
||||
@@ -199,7 +228,7 @@ func savePID(pid int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func monitorNebulaProcess(proc *exec.Cmd) {
|
||||
func monitorNebulaProcess(proc *exec.Cmd, done chan struct{}) {
|
||||
err := proc.Wait()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
@@ -216,26 +245,34 @@ func monitorNebulaProcess(proc *exec.Cmd) {
|
||||
// The process has stopped, so update the global state
|
||||
// Only clear state if this is still the active process (a new one may have been started)
|
||||
ProcessMux.Lock()
|
||||
defer ProcessMux.Unlock()
|
||||
process = nil
|
||||
NebulaStarted = false
|
||||
NATSStarted = false
|
||||
if process == proc {
|
||||
process = nil
|
||||
NebulaStarted = false
|
||||
NATSStarted = false
|
||||
}
|
||||
ProcessMux.Unlock()
|
||||
|
||||
close(done)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
// stopped outside of ProcessMux so the DNS goroutine can never block the lock
|
||||
StopDNS()
|
||||
|
||||
ProcessMux.Lock()
|
||||
defer ProcessMux.Unlock()
|
||||
|
||||
if process != nil {
|
||||
done := processDone
|
||||
|
||||
if err := process.Process.Kill(); err != nil {
|
||||
utils.Error("Failed to kill nebula process", err)
|
||||
}
|
||||
// wait for process to be nil
|
||||
|
||||
// wait for the monitor to report the process exited
|
||||
ProcessMux.Unlock()
|
||||
for process != nil {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
utils.Debug("CCIAJHSODLJALSD")
|
||||
if done != nil {
|
||||
<-done
|
||||
}
|
||||
ProcessMux.Lock()
|
||||
|
||||
@@ -328,12 +365,14 @@ func GetAllDevicesEvenBlocked() ([]utils.ConstellationDevice, error) {
|
||||
var devices []utils.ConstellationDevice
|
||||
|
||||
cursor, err := c.Find(nil, map[string]interface{}{})
|
||||
defer cursor.Close(nil)
|
||||
cursor.All(nil, &devices)
|
||||
|
||||
if err != nil {
|
||||
return []utils.ConstellationDevice{}, err
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err := cursor.All(nil, &devices); err != nil {
|
||||
return []utils.ConstellationDevice{}, err
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
@@ -350,12 +389,14 @@ func GetAllDevices() ([]utils.ConstellationDevice, error) {
|
||||
cursor, err := c.Find(nil, map[string]interface{}{
|
||||
"Blocked": false,
|
||||
})
|
||||
defer cursor.Close(nil)
|
||||
cursor.All(nil, &devices)
|
||||
|
||||
if err != nil {
|
||||
return []utils.ConstellationDevice{}, err
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err := cursor.All(nil, &devices); err != nil {
|
||||
return []utils.ConstellationDevice{}, err
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
@@ -373,12 +414,14 @@ func GetAllLightHouses() ([]utils.ConstellationDevice, error) {
|
||||
"IsLighthouse": true,
|
||||
"Blocked": false,
|
||||
})
|
||||
defer cursor.Close(nil)
|
||||
cursor.All(nil, &devices)
|
||||
|
||||
if err != nil {
|
||||
return []utils.ConstellationDevice{}, err
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err := cursor.All(nil, &devices); err != nil {
|
||||
return []utils.ConstellationDevice{}, err
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
@@ -391,16 +434,6 @@ func ExportDefaultConfigToYAML(outputPath string) error {
|
||||
// Combine defaultConfig
|
||||
finalConfig := NebulaDefaultConfig
|
||||
|
||||
hostnames := []string{}
|
||||
hsraw := strings.Split(utils.GetMainConfig().ConstellationConfig.ConstellationHostname, ",")
|
||||
for _, hostname := range hsraw {
|
||||
// trim
|
||||
hostname = strings.TrimSpace(hostname)
|
||||
if hostname != "" {
|
||||
hostnames = append(hostnames, hostname + ":4242")
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal the combined config to YAML
|
||||
yamlData, err := yaml.Marshal(finalConfig)
|
||||
if err != nil {
|
||||
@@ -450,17 +483,6 @@ func getYAMLClientConfig(name, configPath, capki, cert, key, APIKey string, devi
|
||||
}
|
||||
|
||||
if staticHostMap, ok := configMap["static_host_map"].(map[interface{}]interface{}); ok {
|
||||
hostnames := []string{}
|
||||
hs, _ := GetCurrentDeviceHostname()
|
||||
hsraw := strings.Split(hs, ",")
|
||||
for _, hostname := range hsraw {
|
||||
// trim
|
||||
hostname = strings.TrimSpace(hostname)
|
||||
if hostname != "" {
|
||||
hostnames = append(hostnames, hostname + ":4242")
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range devices {
|
||||
staticHostMap[cleanIp(l.IP)] = []string{}
|
||||
|
||||
@@ -668,11 +690,28 @@ func GetConfigAttribute(configPath string, attr string) (string, error) {
|
||||
return strValue, nil
|
||||
}
|
||||
|
||||
func generateNebulaCert(name, filename, ip, PK string, saveToFile bool) (string, string, string, error) {
|
||||
// getCertPrefixLength returns the prefix length of the configured IP range, defaulting to 24
|
||||
func getCertPrefixLength() string {
|
||||
ipRange := utils.GetMainConfig().ConstellationConfig.IPRange
|
||||
|
||||
if ipRange != "" {
|
||||
if _, ipNet, err := net.ParseCIDR(ipRange); err == nil {
|
||||
if ones, _ := ipNet.Mask.Size(); ones > 0 {
|
||||
return strconv.Itoa(ones)
|
||||
}
|
||||
}
|
||||
|
||||
utils.Warn("Constellation: invalid IPRange " + ipRange + ", defaulting certificate subnet to /24")
|
||||
}
|
||||
|
||||
return "24"
|
||||
}
|
||||
|
||||
func generateNebulaCert(name, filename, ip string, saveToFile bool) (string, string, string, error) {
|
||||
// Run the nebula-cert command
|
||||
var cmd *exec.Cmd
|
||||
|
||||
ip = ip + "/24"
|
||||
ip = ip + "/" + getCertPrefixLength()
|
||||
|
||||
// Read the generated certificate and key files
|
||||
certPath := fmt.Sprintf("./%s.crt", name)
|
||||
@@ -688,33 +727,14 @@ func generateNebulaCert(name, filename, ip, PK string, saveToFile bool) (string,
|
||||
os.Remove(keyPath)
|
||||
}
|
||||
|
||||
if(PK == "") {
|
||||
cmd = exec.Command(certBinaryToRun(),
|
||||
"sign",
|
||||
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
|
||||
"-ca-key", utils.CONFIGFOLDER + "ca.key",
|
||||
"-subnets", "0.0.0.0/0",
|
||||
"-name", name,
|
||||
"-ip", ip,
|
||||
)
|
||||
} else {
|
||||
// write PK to temp.cert
|
||||
err := ioutil.WriteFile("./temp.key", []byte(PK), 0644)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("failed to write temp.key: %s", err)
|
||||
}
|
||||
cmd = exec.Command(certBinaryToRun(),
|
||||
"sign",
|
||||
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
|
||||
"-ca-key", utils.CONFIGFOLDER + "ca.key",
|
||||
"-subnets", "0.0.0.0/0",
|
||||
"-name", name,
|
||||
"-ip", ip,
|
||||
"-in-pub", "./temp.key",
|
||||
)
|
||||
// delete temp.key
|
||||
defer os.Remove("./temp.key")
|
||||
}
|
||||
cmd = exec.Command(certBinaryToRun(),
|
||||
"sign",
|
||||
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
|
||||
"-ca-key", utils.CONFIGFOLDER + "ca.key",
|
||||
"-subnets", "0.0.0.0/0",
|
||||
"-name", name,
|
||||
"-ip", ip,
|
||||
)
|
||||
|
||||
// Get pipes for stdout and stderr
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
@@ -812,9 +832,8 @@ func generateNebulaCACert(name string) error {
|
||||
cmd := exec.Command(
|
||||
certBinaryToRun(),
|
||||
"ca",
|
||||
"-name",
|
||||
"-name", name,
|
||||
"-duration", "87600h", // 10 years
|
||||
"\""+name+"\"",
|
||||
)
|
||||
utils.Debug(cmd.String())
|
||||
|
||||
@@ -1126,38 +1145,70 @@ func GetCurrentDevice() (utils.ConstellationDevice, error) {
|
||||
|
||||
device = utils.ConstellationDevice{}
|
||||
|
||||
if configMap["cstln_device_name"] != nil {
|
||||
device.DeviceName = configMap["cstln_device_name"].(string)
|
||||
if v, exists := configMap["cstln_device_name"]; exists {
|
||||
if deviceName, ok := v.(string); ok {
|
||||
device.DeviceName = deviceName
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_device_name is not a string, skipping")
|
||||
}
|
||||
} else {
|
||||
return utils.ConstellationDevice{}, errors.New("Invalid new config file for resync - missing name")
|
||||
}
|
||||
|
||||
if configMap["cstln_ip"] != nil {
|
||||
device.IP = configMap["cstln_ip"].(string)
|
||||
if v, exists := configMap["cstln_ip"]; exists {
|
||||
if ip, ok := v.(string); ok {
|
||||
device.IP = ip
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_ip is not a string, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if configMap["cstln_public_hostname"] != nil {
|
||||
device.PublicHostname = configMap["cstln_public_hostname"].(string)
|
||||
if v, exists := configMap["cstln_public_hostname"]; exists {
|
||||
if publicHostname, ok := v.(string); ok {
|
||||
device.PublicHostname = publicHostname
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_public_hostname is not a string, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if configMap["cstln_cosmos_node"] != nil {
|
||||
device.CosmosNode = configMap["cstln_cosmos_node"].(int)
|
||||
if v, exists := configMap["cstln_cosmos_node"]; exists {
|
||||
if cosmosNode, ok := v.(int); ok {
|
||||
device.CosmosNode = cosmosNode
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_cosmos_node is not an int, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if configMap["cstln_is_exit_node"] != nil {
|
||||
device.IsExitNode = configMap["cstln_is_exit_node"].(bool)
|
||||
if v, exists := configMap["cstln_is_exit_node"]; exists {
|
||||
if isExitNode, ok := v.(bool); ok {
|
||||
device.IsExitNode = isExitNode
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_is_exit_node is not a bool, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if configMap["cstln_is_relay"] != nil {
|
||||
device.IsRelay = configMap["cstln_is_relay"].(bool)
|
||||
if v, exists := configMap["cstln_is_relay"]; exists {
|
||||
if isRelay, ok := v.(bool); ok {
|
||||
device.IsRelay = isRelay
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_is_relay is not a bool, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if configMap["cstln_is_lighthouse"] != nil {
|
||||
device.IsLighthouse = configMap["cstln_is_lighthouse"].(bool)
|
||||
if v, exists := configMap["cstln_is_lighthouse"]; exists {
|
||||
if isLighthouse, ok := v.(bool); ok {
|
||||
device.IsLighthouse = isLighthouse
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_is_lighthouse is not a bool, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if configMap["cstln_api_key"] != nil {
|
||||
device.APIKey = configMap["cstln_api_key"].(string)
|
||||
if v, exists := configMap["cstln_api_key"]; exists {
|
||||
if apiKey, ok := v.(string); ok {
|
||||
device.APIKey = apiKey
|
||||
} else {
|
||||
utils.Warn("GetCurrentDevice: cstln_api_key is not a string, skipping")
|
||||
}
|
||||
} else {
|
||||
return utils.ConstellationDevice{}, errors.New("Invalid new config file for resync")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package constellation
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
// repoRootDir returns the absolute repository root (two levels up from this package)
|
||||
func repoRootDir(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("testenv: cannot locate source file")
|
||||
}
|
||||
return filepath.Dir(filepath.Dir(filepath.Dir(thisFile)))
|
||||
}
|
||||
|
||||
// setupTestEnv points CONFIGFOLDER at a temp dir, loads a fresh config and resets
|
||||
// all constellation package globals. mutate (optional) tweaks the config before load.
|
||||
// Tests using this must NOT run in parallel: they share package-level state.
|
||||
func setupTestEnv(t *testing.T, mutate func(*utils.Config)) string {
|
||||
t.Helper()
|
||||
|
||||
tmp := t.TempDir() + "/"
|
||||
prevFolder := utils.CONFIGFOLDER
|
||||
utils.CONFIGFOLDER = tmp
|
||||
|
||||
cfg := utils.DefaultConfig
|
||||
cfg.ConstellationConfig.Enabled = true
|
||||
cfg.ConstellationConfig.DNSFallback = "8.8.8.8:53"
|
||||
if mutate != nil {
|
||||
mutate(&cfg)
|
||||
}
|
||||
utils.LoadBaseMainConfig(cfg)
|
||||
|
||||
utils.CloseEmbeddedDB()
|
||||
resetConstellationGlobals()
|
||||
|
||||
t.Cleanup(func() {
|
||||
utils.CloseEmbeddedDB()
|
||||
resetConstellationGlobals()
|
||||
utils.CONFIGFOLDER = prevFolder
|
||||
})
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
func resetConstellationGlobals() {
|
||||
cachedCurrentDevice = nil
|
||||
CachedDevices = map[string]utils.ConstellationDevice{}
|
||||
CachedDeviceNames = map[string]string{}
|
||||
NebulaStarted = false
|
||||
NebulaHasStarted = false
|
||||
NATSStarted = false
|
||||
|
||||
dnsMux.Lock()
|
||||
DNSStarted = false
|
||||
dnsServer = nil
|
||||
dnsStarting = false
|
||||
dnsMux.Unlock()
|
||||
DNSBlacklist = map[string]bool{}
|
||||
}
|
||||
|
||||
// writeNebulaYML writes CONFIGFOLDER/nebula.yml from the given fields and
|
||||
// invalidates the current-device cache. Typical fields: cstln_device_name,
|
||||
// cstln_api_key, cstln_ip, cstln_is_lighthouse...
|
||||
func writeNebulaYML(t *testing.T, fields map[string]interface{}) {
|
||||
t.Helper()
|
||||
|
||||
data, err := yaml.Marshal(fields)
|
||||
if err != nil {
|
||||
t.Fatal("testenv: marshal nebula.yml:", err)
|
||||
}
|
||||
if err := os.WriteFile(utils.CONFIGFOLDER+"nebula.yml", data, 0600); err != nil {
|
||||
t.Fatal("testenv: write nebula.yml:", err)
|
||||
}
|
||||
cachedCurrentDevice = nil
|
||||
}
|
||||
|
||||
// chdirWithCertBinary moves CWD to a temp dir containing symlinks to the
|
||||
// nebula-cert/nebula binaries, since the code resolves them as "./nebula-cert".
|
||||
// Cert files generated with saveToFile=false also land in this CWD.
|
||||
func chdirWithCertBinary(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
root := repoRootDir(t)
|
||||
work := t.TempDir()
|
||||
|
||||
for _, bin := range []string{"nebula-cert", "nebula", "nebula-arm-cert", "nebula-arm"} {
|
||||
src := filepath.Join(root, bin)
|
||||
if _, err := os.Stat(src); err != nil {
|
||||
continue
|
||||
}
|
||||
if err := os.Symlink(src, filepath.Join(work, bin)); err != nil {
|
||||
t.Fatal("testenv: symlink "+bin+":", err)
|
||||
}
|
||||
}
|
||||
|
||||
prev, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("testenv: getwd:", err)
|
||||
}
|
||||
if err := os.Chdir(work); err != nil {
|
||||
t.Fatal("testenv: chdir:", err)
|
||||
}
|
||||
t.Cleanup(func() { os.Chdir(prev) })
|
||||
|
||||
return work
|
||||
}
|
||||
@@ -96,6 +96,9 @@ func StopHeartbeat() {
|
||||
// only consumer in production.
|
||||
pro.StopResourceSampler()
|
||||
|
||||
heartbeatLock.Lock()
|
||||
defer heartbeatLock.Unlock()
|
||||
|
||||
if heartbeatStopChan != nil {
|
||||
close(heartbeatStopChan)
|
||||
heartbeatStopChan = nil
|
||||
@@ -167,12 +170,14 @@ func ClientHeartbeatInit() {
|
||||
|
||||
UpdateLocalTunnelCache()
|
||||
|
||||
heartbeatLock.Lock()
|
||||
heartbeatStopChan = make(chan struct{})
|
||||
heartbeatTicker = time.NewTicker(2 * time.Second)
|
||||
|
||||
// Capture in local variables to avoid race conditions
|
||||
stopChan := heartbeatStopChan
|
||||
ticker := heartbeatTicker
|
||||
heartbeatLock.Unlock()
|
||||
|
||||
// Watch KV for changes and refresh tunnel cache
|
||||
go func() {
|
||||
@@ -329,6 +334,7 @@ var localTunnelCacheMutex = &sync.RWMutex{}
|
||||
var lastCacheUpdate time.Time
|
||||
var heartbeatStopChan chan struct{}
|
||||
var heartbeatTicker *time.Ticker
|
||||
var heartbeatLock sync.Mutex
|
||||
|
||||
func UpdateLocalTunnelCache() {
|
||||
if IsConstellationStandalone() {
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ func main() {
|
||||
}
|
||||
|
||||
// @title Cosmos Server API
|
||||
// @version 0.22.31
|
||||
// @version 0.22.32
|
||||
// @description REST API for Cosmos Cloud server management
|
||||
// @BasePath /cosmos
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
|
||||
+2
-1
@@ -436,7 +436,8 @@ type ConstellationDNSEntry struct {
|
||||
type ConstellationDevice struct {
|
||||
Nickname string `json:"nickname" bson:"Nickname"`
|
||||
DeviceName string `json:"deviceName" bson:"DeviceName"`
|
||||
PublicKey string `json:"publicKey" bson:"PublicKey"`
|
||||
// legacy field: used to hold the device private key, never expose it
|
||||
PublicKey string `json:"-" bson:"PublicKey"`
|
||||
IP string `json:"ip" bson:"IP"`
|
||||
IsLighthouse bool `json:"isLighthouse" bson:"IsLighthouse"`
|
||||
CosmosNode int `json:"cosmosNode" bson:"CosmosNode"`
|
||||
|
||||
Reference in New Issue
Block a user