[skip ci] constellation refact part 1

This commit is contained in:
Yann Stepienik
2026-01-24 17:05:50 +00:00
parent 44e6eee275
commit 7ba8bc1978
19 changed files with 517 additions and 296 deletions
+2 -1
View File
@@ -27,4 +27,5 @@ restic-arm
rclone
rclone-arm
test-backup
/backups
/backups
ca.pem
+13
View File
@@ -114,6 +114,18 @@ function connect(file) {
});
}
function create(deviceName) {
return wrap(fetch(`/cosmos/api/constellation/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
deviceName
}),
}))
}
function block(nickname, devicename, block) {
return wrap(fetch(`/cosmos/api/constellation/block`, {
method: 'POST',
@@ -137,5 +149,6 @@ export {
connect,
block,
ping,
create,
pingDevice,
};
+4 -2
View File
@@ -28,7 +28,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
const {role, nickname} = useClientInfos();
const isAdmin = role === "2";
let firstIP = "192.168.201.2/24";
let firstIP = "192.168.201.2";
if (devices && devices.length > 0) {
const isIpFree = (ip) => {
return devices.filter((d) => d.ip === ip).length === 0;
@@ -41,7 +41,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
i = 0;
j++;
}
firstIP = "192.168." + j + "." + i + "/24";
firstIP = "192.168." + j + "." + i;
}
}
@@ -81,6 +81,8 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
}}
validationSchema={yup.object({
deviceName: yup.string().required().min(3).max(32)
.matches(/^[a-z0-9-]+$/, t('mgmt.constellation.setup.deviceName.validationError')),
})}
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
+30 -5
View File
@@ -9,6 +9,7 @@ import { Alert, Button, Chip, CircularProgress, IconButton, LinearProgress, Stac
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
import MainCard from "../../components/MainCard";
import { Formik } from "formik";
import * as Yup from "yup";
import { LoadingButton } from "@mui/lab";
import ApiModal from "../../components/apiModal";
import { isDomain } from "../../utils/indexs";
@@ -190,18 +191,31 @@ export const ConstellationVPN = ({ freeVersion }) => {
</>}
{(isAdmin || constellationEnabled) && <Formik
enableReinitialize
validationSchema={Yup.object().shape({
DeviceName: Yup.string().required().min(3).max(32)
.matches(/^[a-z0-9-]+$/, t('mgmt.constellation.setup.deviceName.validationError')),
})}
initialValues={{
DeviceName: config.ConstellationConfig.ThisDeviceName || '',
Enabled: config.ConstellationConfig.Enabled,
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
IsRelay: config.ConstellationConfig.IsRelayNode,
IsExitNode: config.ConstellationConfig.IsExitNode,
SyncNodes: !config.ConstellationConfig.DoNotSyncNodes,
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
getDefaultConstellationHostname(config)
}}
onSubmit={(values) => {
onSubmit={async (values) => {
const isCreating = !config.ConstellationConfig.ThisDeviceName;
if (isCreating) {
await API.constellation.create(values.DeviceName);
setTimeout(() => {
refreshConfig();
}, 1500);
return;
}
let newConfig = { ...config };
newConfig.ConstellationConfig.Enabled = values.Enabled;
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
newConfig.ConstellationConfig.IsRelayNode = values.IsRelay;
newConfig.ConstellationConfig.IsExitNode = values.IsExitNode;
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
newConfig.ConstellationConfig.DoNotSyncNodes = !values.SyncNodes;
@@ -318,7 +332,16 @@ export const ConstellationVPN = ({ freeVersion }) => {
</div>}
{!freeVersion && <>
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="Enabled" label={t('mgmt.constellation.setup.enabledCheckbox')} />
<CosmosInputText
disabled={!isAdmin || !!config.ConstellationConfig.ThisDeviceName}
formik={formik}
name="DeviceName"
label={t('mgmt.constellation.setup.deviceName.label')}
/>
{config.ConstellationConfig.ThisDeviceName && (
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="Enabled" label={t('mgmt.constellation.setup.enabledCheckbox')} />
)}
{constellationEnabled && !config.ConstellationConfig.SlaveMode && <>
{formik.values.Enabled && <>
@@ -338,7 +361,9 @@ export const ConstellationVPN = ({ freeVersion }) => {
variant="contained"
color="primary"
>
{t('global.saveAction')}
{config.ConstellationConfig.ThisDeviceName
? t('global.saveAction')
: t('mgmt.constellation.setup.createConstellation')}
</LoadingButton>
</>}
</>}
@@ -251,6 +251,8 @@
"mgmt.constellation.resetLabel": "Reset Network",
"mgmt.constellation.resetText": "This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone.",
"mgmt.constellation.restartButton": "Restart VPN Service",
"mgmt.constellation.setup.createConstellation": "Create Constellation",
"mgmt.constellation.setup.deviceName.validationError": "Device must only contain lowercase letters, numbers and hyphens",
"mgmt.constellation.setup.addDeviceSuccess": "Device added successfully! Download scan the QR Code from the Cosmos app or download the relevant files to your device along side the config and network certificate to connect:",
"mgmt.constellation.setup.addDeviceText": "Add a Device to the constellation using either the Cosmos or Nebula client",
"mgmt.constellation.setup.addDeviceTitle": "Add Device",
-2
View File
@@ -68,7 +68,6 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
for _, q := range r.Question {
for hostname, _destination := range remoteHostnames {
destination := CachedDeviceNames[_destination]
destination = strings.ReplaceAll(destination, "/24", "")
if destination != "" {
if strings.HasSuffix(q.Name, hostname + ".") && q.Qtype == dns.TypeA {
@@ -103,7 +102,6 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
utils.Debug("DNS Question " + q.Name)
for deviceName, ip := range CachedDeviceNames {
procDeviceName := strings.ReplaceAll(deviceName, " ", "-")
ip = strings.ReplaceAll(ip, "/24", "")
if strings.HasSuffix(q.Name, procDeviceName + ".") && q.Qtype == dns.TypeA {
utils.Debug("DNS Overwrite " + procDeviceName + " with its IP")
+135 -31
View File
@@ -7,7 +7,7 @@ import (
"sync"
"strings"
"crypto/tls"
"encoding/json"
"encoding/pem"
"github.com/nats-io/nats-server/v2/server"
@@ -18,6 +18,16 @@ import (
natsClient "github.com/nats-io/nats.go"
)
type NodeHeartbeat struct {
DeviceName string
LastSeen time.Time
IP string
IsRelay bool
IsLighthouse bool
IsExitNode bool
IsCosmosNode bool
}
var ns *server.Server
var MASTERUSER = "SERVERUSER"
var MASTERPWD = utils.GenerateRandomString(24)
@@ -37,7 +47,7 @@ func StartNATS() {
return
}
utils.Log("Starting NATS server...")
utils.Log("[NATS] Starting NATS server on " + GetCurrentDeviceIP() + ":4222")
time.Sleep(2 * time.Second)
@@ -54,19 +64,19 @@ func StartNATS() {
// Decode PEM encoded certificate
certDERBlock, _ := pem.Decode(certPEMBlock)
if certDERBlock == nil {
utils.MajorError("Failed to start NATS: parse certificate PEM", nil)
utils.MajorError("[NATS] Failed to start NATS: parse certificate PEM", nil)
}
// Decode PEM encoded private key
keyDERBlock, _ := pem.Decode(keyPEMBlock)
if keyDERBlock == nil {
utils.MajorError("Failed to start NATS: parse key PEM", nil)
utils.MajorError("[NATS] Failed to start NATS: parse key PEM", nil)
}
// Create tls.Certificate using the original PEM data
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
utils.MajorError("Failed to start NATS: create TLS certificate", err)
utils.MajorError("[NATS] Failed to start NATS: create TLS certificate", err)
}
// Configure the NATS server options
@@ -80,6 +90,7 @@ func StartNATS() {
},
}
// if debug, add debug user
for _, devices := range CachedDevices {
username := sanitizeNATSUsername(devices.DeviceName)
@@ -97,16 +108,42 @@ func StartNATS() {
})
}
natsHost := GetCurrentDeviceIP()
if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
users = append(users, &server.User{
Username: "DEBUG",
Password: "DEBUG",
Permissions: nil,
})
natsHost = "0.0.0.0"
}
opts := &server.Options{
Host: "192.168.201.1",
Host: natsHost,
Port: 4222,
JetStream: true,
StoreDir: utils.CONFIGFOLDER + "/jetstream",
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.NoClientCert,
InsecureSkipVerify: true,
},
// Cluster: server.ClusterOpts{
// Host: GetCurrentDeviceIP(),
// Port: 6222,
// TLSConfig: &tls.Config{
// Certificates: []tls.Certificate{cert},
// ClientAuth: tls.NoClientCert,
// InsecureSkipVerify: true,
// },
// },
Users: users,
}
@@ -121,7 +158,7 @@ func StartNATS() {
continue
}
if NebulaFailedStarting {
utils.Error("Nebula failed to start, aborting NATS server setup", nil)
utils.Error("[NATS] Nebula failed to start, aborting NATS server setup", nil)
return
}
@@ -130,18 +167,18 @@ func StartNATS() {
// Wait for the server to be ready
if !ns.ReadyForConnections(time.Duration(2 * (retries + 1)) * time.Second) {
retries++
utils.Debug("NATS server not ready...")
utils.Debug("[NATS] NATS server not ready...")
err = errors.New("NATS server not ready")
continue
}
utils.Debug("Retrying to start NATS server")
utils.Debug("[NATS] Retrying to start NATS server")
}
if err != nil {
utils.MajorError("Error starting NATS server", err)
utils.MajorError("[NATS] Error starting NATS server", err)
} else {
utils.Log("Started NATS server on host " + opts.Host + ":" + strconv.Itoa(opts.Port))
utils.Log("[NATS] Started NATS server on host " + opts.Host + ":" + strconv.Itoa(opts.Port))
if !utils.GetMainConfig().ConstellationConfig.SlaveMode {
InitNATSClient()
}
@@ -149,7 +186,7 @@ func StartNATS() {
}
func StopNATS() {
utils.Log("Stopping NATS server...")
utils.Log("[NATS] Stopping NATS server...")
if ns != nil {
ns.Shutdown()
@@ -162,6 +199,7 @@ func StopNATS() {
var clientConfigLock = sync.Mutex{}
var NATSClientTopic = ""
var nc *nats.Conn
var js nats.JetStreamContext
func InitNATSClient() {
if nc != nil {
return
@@ -174,11 +212,11 @@ func InitNATSClient() {
retries := 0
if NebulaFailedStarting {
utils.Error("Nebula failed to start, aborting NATS client connection", nil)
utils.Error("[NATS] Nebula failed to start, aborting NATS client connection", nil)
return
}
utils.Log("Connecting to NATS server...")
utils.Log("[NATS] Connecting to NATS server...")
time.Sleep(2 * time.Second)
@@ -186,7 +224,7 @@ func InitNATSClient() {
user = sanitizeNATSUsername(user)
if err != nil {
utils.MajorError("Error getting constellation credentials", err)
utils.MajorError("[NATS] Error getting constellation credentials", err)
return
}
@@ -205,7 +243,7 @@ func InitNATSClient() {
for err != nil {
if retries == 10 {
utils.MajorError("Error connecting to Constellation NATS server (timeout) - will continue trying", err)
utils.MajorError("[NATS] Error connecting to Constellation NATS server (timeout) - will continue trying", err)
}
if retries >= 11 {
@@ -213,7 +251,7 @@ func InitNATSClient() {
}
if NebulaFailedStarting {
utils.Error("Nebula failed to start, aborting NATS client connection", nil)
utils.Error("[NATS] Nebula failed to start, aborting NATS client connection", nil)
return
}
@@ -232,19 +270,26 @@ func InitNATSClient() {
if err != nil {
retries++
utils.Debug("Retrying to start NATS Client: " + err.Error())
utils.Debug("[NATS] Retrying to start NATS Client: " + err.Error())
}
}
if err != nil {
utils.MajorError("Error connecting to Constellation NATS server", err)
utils.MajorError("[NATS] Error connecting to Constellation NATS server", err)
return
} else {
utils.Log("Connected to NATS server as " + user)
utils.Log("[NATS] Connected to NATS server as " + user)
NATSClientTopic = "cosmos." + user
}
utils.Debug("NATS client connected")
utils.Debug("[NATS] NATS client connected")
js, err = nc.JetStream()
if err != nil {
utils.MajorError("[NATS] Error getting JetStream context", err)
}
utils.Debug("[NATS] JetStream context obtained")
if !utils.GetMainConfig().ConstellationConfig.SlaveMode {
go MasterNATSClientRouter()
@@ -255,6 +300,65 @@ func InitNATSClient() {
RequestSyncPayload()
clientConfigLock.Lock()
}
ClientHeartbeatInit()
// POST CLIENT CONNECTION HOOK
}
func ClientHeartbeatInit() {
var kv nats.KeyValue
var err error
kv, err = js.KeyValue("constellation-nodes")
if err != nil {
kv, err = js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: "constellation-nodes",
TTL: 10 * time.Second,
})
if err != nil {
utils.MajorError("[NATS] Error creating Key-Value store", err)
return
}
}
utils.Debug("[NATS] Key-Value store 'constellation-nodes' ready")
go func() {
device := GetCurrentDevice()
key := sanitizeNATSUsername(device.DeviceName)
ticker := time.NewTicker(2 * time.Second)
for range ticker.C {
if !IsClientConnected() {
utils.Warn("[NATS] NATS client not connected during heartbeat")
InitNATSClient()
}
utils.Debug("[NATS] Updating heartbeat for " + device.DeviceName)
// insert JSON encoded heartbeat
heartbeat := NodeHeartbeat{
DeviceName: device.DeviceName,
LastSeen: time.Now(),
IP: device.IP,
IsRelay: device.IsRelay,
IsLighthouse: device.IsLighthouse,
IsExitNode: device.IsExitNode,
IsCosmosNode: device.IsCosmosNode,
}
heartbeatData, err := json.Marshal(heartbeat)
if err != nil {
utils.Error("[NATS] Error marshalling heartbeat JSON", err)
continue
}
_, err = kv.Put(key, heartbeatData)
if err != nil {
utils.Error("[NATS] Error updating heartbeat in Key-Value store", err)
}
}
}()
}
func IsClientConnected() bool {
@@ -312,7 +416,7 @@ func PublishNATSMessage(topic string, payload string) error {
// Send a request and wait for a response
err := nc.Publish(topic, []byte(payload))
if err != nil {
utils.Error("Error sending request", err)
utils.Error("[NATS] Error sending request", err)
return err
}
@@ -320,7 +424,7 @@ func PublishNATSMessage(topic string, payload string) error {
}
func MasterNATSClientRouter() {
utils.Log("Starting NATS Master client router.")
utils.Log("[NATS] Starting NATS Master client router.")
nc.Subscribe("cosmos."+MASTERUSER+".ping", func(m *nats.Msg) {
utils.Debug("[MQ] Received: " + string(m.Data) + " from " + m.Subject)
@@ -366,20 +470,20 @@ func MasterNATSClientRouter() {
func SlaveNATSClientRouter() {
utils.Log("Starting NATS Slave client router.")
username := sanitizeNATSUsername(DeviceName)
username := sanitizeNATSUsername(GetCurrentDeviceName())
nc.Subscribe("cosmos."+username+".constellation.config.resync", func(m *nats.Msg) {
utils.Log("Constellation config changed, resyncing...")
utils.Log("[NATS] Constellation config changed, resyncing...")
config := m.Data
needRestart, err := SlaveConfigSync((string)(config))
if err != nil {
utils.MajorError("Error re-syncing Constellation config, please manually sync", err)
utils.MajorError("[NATS] Error re-syncing Constellation config, please manually sync", err)
} else {
if needRestart {
utils.Warn("Slave config has changed, restarting Nebula...")
utils.Warn("[NATS] Slave config has changed, restarting Nebula...")
RestartNebula()
utils.RestartHTTPServer()
}
@@ -387,7 +491,7 @@ func SlaveNATSClientRouter() {
})
nc.Subscribe("cosmos."+username+".constellation.data.sync-receive", func(m *nats.Msg) {
utils.Log("Constellation data sync received")
utils.Log("[NATS] Constellation data sync received")
payload := m.Data
@@ -398,7 +502,7 @@ func SlaveNATSClientRouter() {
func PingNATSClient() bool {
user, _, err := GetNATSCredentials(!utils.GetMainConfig().ConstellationConfig.SlaveMode)
if err != nil {
utils.Error("Error getting constellation credentials", err)
utils.Error("[NATS] Error getting constellation credentials", err)
return false
}
@@ -406,12 +510,12 @@ func PingNATSClient() bool {
response, err := SendNATSMessage("cosmos."+user+".ping", "Ping")
if err != nil {
utils.Error("Error pinging NATS client", err)
utils.Error("[NATS] Error pinging NATS client", err)
return false
}
if response != "" {
utils.Debug("NATS client response: " + response)
utils.Debug("[NATS] NATS client response: " + response)
return true
}
+4 -27
View File
@@ -126,7 +126,7 @@ func GetDeviceConfigSync(w http.ResponseWriter, req *http.Request) {
utils.Log("DeviceConfigSync: Fetching devices for IP " + ip)
cursor, err := c.Find(nil, map[string]interface{}{
"IP": ip + "/24",
"IP": ip,
"APIKey": auth,
"Blocked": false,
})
@@ -354,29 +354,15 @@ func GetNATSCredentials(isMaster bool) (string, string, error) {
if isMaster {
return MASTERUSER, MASTERPWD, nil
}
nebulaFile, err := ioutil.ReadFile(utils.CONFIGFOLDER + "nebula.yml")
if err != nil {
utils.Error("GetNATSCredentials: error while reading nebula.yml", err)
return "", "", err
}
configMap := make(map[string]interface{})
err = yaml.Unmarshal(nebulaFile, &configMap)
if err != nil {
utils.Error("GetNATSCredentials: Invalid slave config file for resync", err)
return "", "", err
}
currentDevice := GetCurrentDevice()
if configMap["cstln_api_key"] == nil || configMap["cstln_device_name"] == nil {
if currentDevice.APIKey == "" || currentDevice.DeviceName == "" {
utils.Error("GetNATSCredentials: Invalid slave config file for resync", nil)
return "", "", errors.New("Invalid slave config file for resync")
}
apiKey := configMap["cstln_api_key"].(string)
deviceName := configMap["cstln_device_name"].(string)
return deviceName, apiKey, nil
return currentDevice.DeviceName, currentDevice.APIKey, nil
}
func SlaveConfigSync(newConfig string) (bool, error) {
@@ -410,15 +396,6 @@ func SlaveConfigSync(newConfig string) (bool, error) {
endpoint := rawEndpoint.(string)
endpoint += "cosmos/api/constellation/config-sync"
// utils.Log("SlaveConfigSync: Fetching config from " + endpoint.(string))
// fetch the config from the endpoint with Authorization header
// req, err := http.NewRequest("GET", endpoint.(string), nil)
// if err != nil {
// utils.Error("SlaveConfigSync: Error creating request", err)
// return false, err
// }
body := newConfig
if body == "" {
+136 -145
View File
@@ -3,6 +3,7 @@ package constellation
import (
"net/http"
"encoding/json"
"errors"
"go.mongodb.org/mongo-driver/mongo"
"github.com/azukaar/cosmos-server/src/utils"
@@ -25,9 +26,11 @@ type DeviceCreateRequestJSON struct {
PublicHostname string `json:"PublicHostname",omitempty`
Port string `json:"port",omitempty`
// internal
APIKey string `json:"-"`
}
func DeviceCreate(w http.ResponseWriter, req *http.Request) {
func DeviceCreate_API(w http.ResponseWriter, req *http.Request) {
if(req.Method == "POST") {
var request DeviceCreateRequestJSON
err1 := json.NewDecoder(req.Body).Decode(&request)
@@ -45,6 +48,12 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
return
}
nickname := utils.Sanitize(request.Nickname)
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return
}
errV := utils.Validate.Struct(request)
if errV != nil {
utils.Error("DeviceCreation: Invalid User Request", errV)
@@ -52,147 +61,21 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
http.StatusInternalServerError, "DC002")
return
}
nickname := utils.Sanitize(request.Nickname)
deviceName := utils.Sanitize(request.DeviceName)
APIKey := utils.GenerateRandomString(32)
// name cannot be "cosmos"
if deviceName == "cosmos" {
utils.Error("DeviceCreation: Device name cannot be 'cosmos'", nil)
utils.HTTPError(w, "Device Creation Error: Device name cannot be 'cosmos'",
http.StatusBadRequest, "DC008")
return
}
cert, key, _, request, err := DeviceCreate(request)
APIKey := request.APIKey
deviceName := request.DeviceName
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
capki, err := getCApki()
if err != nil {
utils.Error("DeviceCreation: Error while reading CA", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC003")
return
}
utils.Log("ConstellationDeviceCreation: Creating 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")
return
}
device := utils.Device{}
utils.Debug("ConstellationDeviceCreation: Creating Device " + deviceName)
err2 := c.FindOne(nil, map[string]interface{}{
"DeviceName": deviceName,
"Blocked": false,
}).Decode(&device)
if err2 == mongo.ErrNoDocuments {
cert, key, fingerprint, err := generateNebulaCert(deviceName, request.IP, request.PublicKey, false)
if err != nil {
utils.Error("DeviceCreation: Error while creating Device", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC001")
return
}
// Cosmos nodes are also lighthouses
if request.IsCosmosNode {
request.IsLighthouse = true
}
// Check cosmos node and devices limit
if request.IsCosmosNode {
totalClientLimit := 10 * int64(utils.GetNumberUsers())
count, errCount := c.CountDocuments(nil, map[string]interface{}{
"IsCosmosNode": true,
"Blocked": false,
})
if errCount != nil {
utils.Error("DeviceCreation: Error while counting cosmos nodes", errCount)
utils.HTTPError(w, "Device Creation Error", http.StatusInternalServerError, "DC009")
return
}
countDevices, errCountDevices := c.CountDocuments(nil, map[string]interface{}{
"Blocked": false,
})
if errCountDevices != nil {
utils.Error("DeviceCreation: Error while counting devices", errCountDevices)
utils.HTTPError(w, "Device Creation Error", http.StatusInternalServerError, "DC011")
return
}
if countDevices >= totalClientLimit {
utils.Error("DeviceCreation: Device limit reached", nil)
utils.HTTPError(w, "Device limit reached", http.StatusConflict, "DC012")
return
}
if count >= int64(utils.GetNumberCosmosNode()) {
utils.Error("DeviceCreation: Cosmos node limit reached", nil)
utils.HTTPError(w, "Cosmos node limit reached", http.StatusConflict, "DC010")
return
}
}
if request.IsLighthouse && request.Nickname != "" {
utils.Error("DeviceCreation: Lighthouse cannot belong to a user", nil)
utils.HTTPError(w, "Device Creation Error: Lighthouse cannot have a nickname",
http.StatusInternalServerError, "DC003")
return
}
if err != nil {
utils.Error("DeviceCreation: Error while getting fingerprint", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC007")
return
}
_, err3 := c.InsertOne(nil, map[string]interface{}{
"Nickname": nickname,
"DeviceName": deviceName,
"PublicKey": key,
"IP": request.IP,
"IsLighthouse": request.IsLighthouse,
"IsCosmosNode": request.IsCosmosNode,
"IsRelay": request.IsLighthouse && request.IsRelay,
"IsExitNode": request.IsLighthouse && request.IsExitNode,
"PublicHostname": request.PublicHostname,
"Port": request.Port,
"Fingerprint": fingerprint,
"APIKey": APIKey,
"Blocked": false,
"Invisible": request.Invisible && !request.IsLighthouse,
})
if err3 != nil {
utils.Error("DeviceCreation: Error while creating Device", err3)
utils.HTTPError(w, "Device Creation Error: " + err3.Error(),
http.StatusInternalServerError, "DC004")
return
}
capki, err := getCApki()
if err != nil {
utils.Error("DeviceCreation: Error while reading ca.crt", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC006")
return
}
lightHousesList := []utils.ConstellationDevice{}
if request.IsLighthouse {
lightHousesList, err = GetAllLightHouses()
}
if err == nil {
// read configYml from config/nebula.yml
configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml", capki, cert, key, APIKey, utils.ConstellationDevice{
Nickname: nickname,
@@ -209,6 +92,11 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
}, true, true)
lightHousesList := []utils.ConstellationDevice{}
if request.IsLighthouse {
lightHousesList, err = GetAllLightHouses()
}
if err != nil {
utils.Error("DeviceCreation: Error while reading config", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
@@ -250,19 +138,122 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
})
go RestartNebula()
} else if err2 == nil {
utils.Error("DeviceCreation: Device already exists", nil)
utils.HTTPError(w, "Device name already exists", http.StatusConflict, "DC002")
return
} else {
utils.Error("DeviceCreation: Error while finding device", err2)
utils.HTTPError(w, "Device Creation Error: " + err2.Error(),
http.StatusInternalServerError, "DC001")
return
utils.Error("DeviceCreation: Error creating device", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
http.StatusInternalServerError, "DC004")
return
}
} else {
utils.Error("DeviceCreation: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func DeviceCreate(request DeviceCreateRequestJSON) (string, string, string, DeviceCreateRequestJSON, error) {
nickname := utils.Sanitize(request.Nickname)
deviceName := utils.Sanitize(request.DeviceName)
APIKey := utils.GenerateRandomString(32)
utils.Log("ConstellationDeviceCreation: Creating Device " + deviceName)
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
defer closeDb()
if errCo != nil {
return "", "", "", DeviceCreateRequestJSON{}, errCo
}
device := utils.Device{}
utils.Debug("ConstellationDeviceCreation: Creating Device " + deviceName)
err2 := c.FindOne(nil, map[string]interface{}{
"DeviceName": deviceName,
"Blocked": false,
}).Decode(&device)
if err2 == mongo.ErrNoDocuments {
cert, key, fingerprint, err := generateNebulaCert(deviceName, request.IP, request.PublicKey, false)
if err != nil {
return "", "", "", DeviceCreateRequestJSON{}, err
}
// Cosmos nodes are also lighthouses
if request.IsCosmosNode {
request.IsLighthouse = true
}
// Check cosmos node and devices limit
if request.IsCosmosNode {
totalClientLimit := 10 * int64(utils.GetNumberUsers())
count, errCount := c.CountDocuments(nil, map[string]interface{}{
"IsCosmosNode": true,
"Blocked": false,
})
if errCount != nil {
return "", "", "", DeviceCreateRequestJSON{}, errCount
}
countDevices, errCountDevices := c.CountDocuments(nil, map[string]interface{}{
"Blocked": false,
})
if errCountDevices != nil {
return "", "", "", DeviceCreateRequestJSON{}, errCountDevices
}
if countDevices >= totalClientLimit {
return "", "", "", DeviceCreateRequestJSON{}, errors.New("DeviceCreation: Device limit reached")
}
if count >= int64(utils.GetNumberCosmosNode()) {
return "", "", "", DeviceCreateRequestJSON{}, errors.New("DeviceCreation: Cosmos Node limit reached")
}
}
if request.IsLighthouse && request.Nickname != "" {
return "", "", "", DeviceCreateRequestJSON{}, errors.New("DeviceCreation: Lighthouse cannot belong to a user")
}
if err != nil {
return "", "", "", DeviceCreateRequestJSON{}, err
}
_, err3 := c.InsertOne(nil, map[string]interface{}{
"Nickname": nickname,
"DeviceName": deviceName,
"PublicKey": key,
"IP": request.IP,
"IsLighthouse": request.IsLighthouse,
"IsCosmosNode": request.IsCosmosNode,
"IsRelay": request.IsLighthouse && request.IsRelay,
"IsExitNode": request.IsLighthouse && request.IsExitNode,
"PublicHostname": request.PublicHostname,
"Port": request.Port,
"Fingerprint": fingerprint,
"APIKey": APIKey,
"Blocked": false,
"Invisible": request.Invisible && !request.IsLighthouse,
})
if err3 != nil {
return "", "", "", DeviceCreateRequestJSON{}, err3
}
request.Nickname = nickname
request.DeviceName = deviceName
request.PublicKey = key
request.IsRelay = request.IsLighthouse && request.IsRelay
request.IsExitNode = request.IsLighthouse && request.IsExitNode
request.Invisible = request.Invisible && !request.IsLighthouse
return cert, key, fingerprint, request, nil
} else if err2 == nil {
return "", "", "", DeviceCreateRequestJSON{}, errors.New("DeviceCreation: Device with this name already exists")
} else {
return "", "", "", DeviceCreateRequestJSON{}, err2
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ func ConstellationAPIDevices(w http.ResponseWriter, req *http.Request) {
if (req.Method == "GET") {
DeviceList(w, req)
} else if (req.Method == "POST") {
DeviceCreate(w, req)
DeviceCreate_API(w, req)
} else {
utils.Error("UserRoute: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+1 -1
View File
@@ -79,7 +79,7 @@ func DevicePublicList(w http.ResponseWriter, req *http.Request) {
IP: "192.168.201.1",
IsLighthouse: true,
IsCosmosNode: true,
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
IsRelay: config.ConstellationConfig.IsRelayNode,
IsExitNode: config.ConstellationConfig.IsExitNode,
PublicHostname: config.ConstellationConfig.ConstellationHostname,
Port: "4242",
+1 -1
View File
@@ -37,7 +37,7 @@ func CheckConstellationToken(req *http.Request) error {
utils.Log("DeviceConfigSync: Fetching devices for IP " + ip)
cursor, err := c.Find(nil, map[string]interface{}{
"IP": ip + "/24",
"IP": ip,
"APIKey": auth,
"Blocked": false,
})
+111 -2
View File
@@ -4,11 +4,111 @@ import (
"net/http"
"encoding/json"
"io/ioutil"
"os"
"gopkg.in/yaml.v2"
"github.com/azukaar/cosmos-server/src/utils"
"github.com/azukaar/cosmos-server/src/utils"
)
func API_NewConstellation(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if req.Method == "POST" {
utils.ConfigLock.Lock()
defer utils.ConfigLock.Unlock()
utils.Log("API_NewConstellation: creating new Constellation")
var request struct {
DeviceName string `json:"deviceName"`
}
err := json.NewDecoder(req.Body).Decode(&request)
if err != nil {
utils.Error("API_NewConstellation: Invalid User Request", err)
utils.HTTPError(w, "API_NewConstellation Error",
http.StatusInternalServerError, "ANC001")
return
}
if request.DeviceName == "" {
utils.Error("API_NewConstellation: Device name is required", nil)
utils.HTTPError(w, "Device name is required",
http.StatusBadRequest, "ANC002")
return
}
// check if ca.crt exists
if _, err = os.Stat(utils.CONFIGFOLDER + "ca.crt"); os.IsNotExist(err) {
utils.Log("Constellation: ca.crt not found, generating...")
// generate ca.crt
errG := generateNebulaCACert("Cosmos - " + utils.GetMainConfig().ConstellationConfig.ConstellationHostname)
if errG != nil {
utils.Error("Constellation: error while generating ca.crt", errG)
}
}
utils.Log("Constellation: cosmos.crt generating...")
// generate cosmos.crt
_,_,_,errG := generateNebulaCert("cosmos", "192.168.201.1", "", true)
if errG != nil {
utils.Error("Constellation: error while generating cosmos.crt", errG)
}
DeviceCreateRequest := DeviceCreateRequestJSON{
DeviceName: request.DeviceName,
IP: "192.168.201.1",
IsLighthouse: true,
IsCosmosNode: true,
Nickname: "",
}
_, _, _, response, err := DeviceCreate(DeviceCreateRequest)
if err != nil {
utils.Error("API_NewConstellation: Error creating lighthouse device", err)
utils.HTTPError(w, "API_NewConstellation Error: " + err.Error(),
http.StatusInternalServerError, "ANC003")
return
}
deviceName := response.DeviceName
config := utils.ReadConfigFromFile()
config.ConstellationConfig.Enabled = true
config.ConstellationConfig.ThisDeviceName = deviceName
utils.SetBaseMainConfig(config)
utils.TriggerEvent(
"cosmos.settings",
"Settings updated",
"success",
"",
map[string]interface{}{
"from": "Constellation",
})
utils.Log("API_NewConstellation: Constellation created with device name: " + deviceName)
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
go func() {
RestartNebula()
utils.RestartHTTPServer()
}()
} else {
utils.Error("API_NewConstellation: Method not allowed " + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func API_ConnectToExisting(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
@@ -55,6 +155,15 @@ func API_ConnectToExisting(w http.ResponseWriter, req *http.Request) {
// output utils.CONFIGFOLDER + "nebula.yml"
err = ioutil.WriteFile(utils.CONFIGFOLDER + "nebula.yml", configMapString, 0644)
if deviceNameVal, ok := configMap["cstln_device_name"]; ok {
config.ConstellationConfig.ThisDeviceName = deviceNameVal.(string)
} else {
utils.Error("API_ConnectToExisting: device name not found in config", nil)
utils.HTTPError(w, "API_ConnectToExisting Error: device name not found in config",
http.StatusInternalServerError, "ACE003")
return
}
// read values into main config
if exitNodeVal, ok := configMap["cstln_is_exit_node"]; ok {
+15 -1
View File
@@ -156,6 +156,20 @@ func UpdateFirewallBlockedClients() error {
"proto": "any",
"host": "any",
})
// Always allow port 6222 (NATS Cluster) from any
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
"port": 6222,
"proto": "any",
"host": "any",
})
// Always allow port 7422 (NATS Leaf) from any
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
"port": 7422,
"proto": "any",
"host": "any",
})
// Always allow port 53 (DNS) from any
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
@@ -512,4 +526,4 @@ func ValidateStaticHosts(logBuffer *lumberjack.Logger) error {
logger.Printf("Updated nebula-temp.yml after validation")
return nil
}
}
+20 -69
View File
@@ -2,18 +2,13 @@ package constellation
import (
"github.com/azukaar/cosmos-server/src/utils"
"os"
"time"
"strings"
"io/ioutil"
"gopkg.in/yaml.v2"
)
var NebulaStarted = false
var CachedDeviceNames = map[string]string{}
var CachedDevices = map[string]utils.ConstellationDevice{}
var DeviceName = ""
var APIKey = ""
func resyncConstellationNodes() {
if utils.GetMainConfig().ConstellationConfig.Enabled {
@@ -24,6 +19,25 @@ func resyncConstellationNodes() {
}
func Init() {
InitConfig()
// if no hostname yet, set default one
if utils.GetMainConfig().ConstellationConfig.ConstellationHostname == "" {
utils.Log("Constellation: no hostname found, setting default one...")
hostnames, _ := utils.ListIps(true)
httpHostname := utils.GetMainConfig().HTTPConfig.Hostname
if(utils.IsDomain(httpHostname)) {
hostnames = append(hostnames, "vpn." + httpHostname)
} else if httpHostname != "127.0.0.1" && httpHostname != "localhost" {
hostnames = append(hostnames, httpHostname)
}
configFile := utils.ReadConfigFromFile()
configFile.ConstellationConfig.ConstellationHostname = strings.Join(hostnames, ", ")
utils.SetBaseMainConfig(configFile)
} else {
utils.Log("Constellation: hostname found: " + utils.GetMainConfig().ConstellationConfig.ConstellationHostname)
}
utils.ResyncConstellationNodes = resyncConstellationNodes
utils.ConstellationSlaveIPWarning = ""
@@ -50,48 +64,8 @@ func Init() {
return
}
InitConfig()
utils.Log("Initializing Constellation module...")
// if no hostname yet, set default one
if utils.GetMainConfig().ConstellationConfig.ConstellationHostname == "" {
utils.Log("Constellation: no hostname found, setting default one...")
hostnames, _ := utils.ListIps(true)
httpHostname := utils.GetMainConfig().HTTPConfig.Hostname
if(utils.IsDomain(httpHostname)) {
hostnames = append(hostnames, "vpn." + httpHostname)
} else if httpHostname != "127.0.0.1" && httpHostname != "localhost" {
hostnames = append(hostnames, httpHostname)
}
configFile := utils.ReadConfigFromFile()
configFile.ConstellationConfig.ConstellationHostname = strings.Join(hostnames, ", ")
utils.SetBaseMainConfig(configFile)
} else {
utils.Log("Constellation: hostname found: " + utils.GetMainConfig().ConstellationConfig.ConstellationHostname)
}
// check if ca.crt exists
if _, err = os.Stat(utils.CONFIGFOLDER + "ca.crt"); os.IsNotExist(err) {
utils.Log("Constellation: ca.crt not found, generating...")
// generate ca.crt
errG := generateNebulaCACert("Cosmos - " + utils.GetMainConfig().ConstellationConfig.ConstellationHostname)
if errG != nil {
utils.Error("Constellation: error while generating ca.crt", errG)
}
}
// check if cosmos.crt exists
if _, err := os.Stat(utils.CONFIGFOLDER + "cosmos.crt"); os.IsNotExist(err) {
utils.Log("Constellation: cosmos.crt not found, generating...")
// generate cosmos.crt
_,_,_,errG := generateNebulaCert("cosmos", "192.168.201.1/24", "", true)
if errG != nil {
utils.Error("Constellation: error while generating cosmos.crt", errG)
}
}
// export nebula.yml
utils.Log("Constellation: exporting nebula.yml...")
err := ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml")
@@ -140,29 +114,6 @@ func Init() {
}
}
// cache device name and api key
nebulaFile, err := ioutil.ReadFile(utils.CONFIGFOLDER + "nebula.yml")
if err == nil {
configMap := make(map[string]interface{})
err = yaml.Unmarshal(nebulaFile, &configMap)
if err != nil {
utils.Error("Constellation: error while unmarshalling nebula.yml", err)
} else {
if configMap["cstln_device_name"] == nil || configMap["cstln_api_key"] == nil {
utils.Warn("Constellation: device name or api key not found in nebula.yml")
DeviceName = ""
APIKey = ""
} else {
DeviceName = configMap["cstln_device_name"].(string)
APIKey = configMap["cstln_api_key"].(string)
}
}
} else {
utils.Error("Constellation: error while reading nebula.yml", err)
DeviceName = ""
APIKey = ""
}
// Does not work because of Digital Ocean's floating IP's gateway system
// if utils.GetMainConfig().ConstellationConfig.SlaveMode {
// // check if the IP
@@ -188,7 +139,7 @@ func Init() {
}
if utils.GetMainConfig().ConstellationConfig.SlaveMode {
go (func() {
go (func() {
InitNATSClient()
var err error
+33 -3
View File
@@ -71,6 +71,9 @@ func startNebulaInBackground() error {
UpdateFirewallBlockedClients()
AdjustDNS(logBuffer)
// removed because cannot use multiple hostnames
// AdjustConfigHostname()
// removed because no retry logic if a node is disconnected: let Nebula handle it
//ValidateStaticHosts(logBuffer)
NebulaFailedStarting = false
@@ -273,6 +276,7 @@ func ResetNebula() error {
config.ConstellationConfig.SlaveMode = false
config.ConstellationConfig.DNSDisabled = false
config.ConstellationConfig.FirewallBlockedClients = []string{}
config.ConstellationConfig.ThisDeviceName = ""
utils.SetBaseMainConfig(config)
@@ -389,7 +393,7 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
// if no lighthouses, be one
finalConfig.Lighthouse.AMLighthouse = len(finalConfig.Lighthouse.Hosts) == 0
finalConfig.Relay.AMRelay = overwriteConfig.NebulaConfig.Relay.AMRelay
finalConfig.Relay.AMRelay = overwriteConfig.IsRelayNode
finalConfig.Relay.Relays = []string{}
for _, l := range lh {
@@ -504,7 +508,7 @@ func getYAMLClientConfig(name, configPath, capki, cert, key, APIKey string, devi
relayMap["am_relay"] = device.IsRelay && device.IsLighthouse
relayMap["use_relays"] = !(device.IsRelay && device.IsLighthouse)
relayMap["relays"] = []string{}
if utils.GetMainConfig().ConstellationConfig.NebulaConfig.Relay.AMRelay {
if utils.GetMainConfig().ConstellationConfig.IsRelayNode {
relayMap["relays"] = append(relayMap["relays"].([]string), "192.168.201.1")
}
@@ -771,6 +775,8 @@ func GetConfigAttribute(configPath string, attr string) (string, error) {
func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, string, error) {
// Run the nebula-cert command
var cmd *exec.Cmd
ip = ip + "/24"
// Read the generated certificate and key files
certPath := fmt.Sprintf("./%s.crt", name)
@@ -998,7 +1004,7 @@ func generateNebulaCACert(name string) error {
}
func GetDeviceIp(device string) string {
return strings.ReplaceAll(CachedDeviceNames[device], "/24", "")
return CachedDeviceNames[device]
}
func populateIPTableMasquerade() {
@@ -1136,4 +1142,28 @@ func pingLighthouse(lh utils.ConstellationDevice, retries int) {
} else {
utils.Debug("Constellation: Lighthouse " + lh.IP + " (" + cleanIp(lh.IP) + ") is reachable")
}
}
func GetCurrentDevice() utils.ConstellationDevice {
config := utils.GetMainConfig()
name := config.ConstellationConfig.ThisDeviceName
return CachedDevices[name]
}
func GetCurrentDeviceName() string {
config := utils.GetMainConfig()
name := config.ConstellationConfig.ThisDeviceName
return name
}
func GetCurrentDeviceAPIKey() string {
config := utils.GetMainConfig()
name := config.ConstellationConfig.ThisDeviceName
return CachedDevices[name].APIKey
}
func GetCurrentDeviceIP() string {
config := utils.GetMainConfig()
name := config.ConstellationConfig.ThisDeviceName
return CachedDevices[name].IP
}
+1
View File
@@ -548,6 +548,7 @@ func InitServer() *mux.Router {
srapiAdmin.HandleFunc("/api/constellation/restart", constellation.API_Restart)
srapiAdmin.HandleFunc("/api/constellation/reset", constellation.API_Reset)
srapiAdmin.HandleFunc("/api/constellation/connect", constellation.API_ConnectToExisting)
srapiAdmin.HandleFunc("/api/constellation/create", constellation.API_NewConstellation)
srapiAdmin.HandleFunc("/api/constellation/config", constellation.API_GetConfig)
srapiAdmin.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
srapiAdmin.HandleFunc("/api/constellation/block", constellation.DeviceBlock)
+2 -2
View File
@@ -81,9 +81,9 @@ func AddConstellationToken(route utils.ProxyRouteConfig) func(next http.Handler)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If the request is from a Constellation tunnel, add the token
if route.TunnelVia == constellation.DeviceName {
if route.TunnelVia == constellation.GetCurrentDeviceName() {
// Add the token
r.Header.Set("x-cstln-auth", constellation.APIKey)
r.Header.Set("x-cstln-auth", constellation.GetCurrentDeviceAPIKey())
}
next.ServeHTTP(w, r)
+6 -3
View File
@@ -292,15 +292,18 @@ type ConstellationConfig struct {
DNSDisabled bool
DNSPort string
DNSFallback string
IsExitNode bool
DNSBlockBlacklist bool
DNSAdditionalBlocklists []string
CustomDNSEntries []ConstellationDNSEntry
NebulaConfig NebulaConfig
ConstellationHostname string
Tunnels []ProxyRouteConfig
FirewallBlockedClients []string `json:"FirewallBlockedClients" bson:"FirewallBlockedClients"`
OverrideNebulaExitNodeInterface string
ThisDeviceName string
// TODO REMOVE
ConstellationHostname string
IsExitNode bool
IsRelayNode bool
}
type ConstellationDNSEntry struct {