diff --git a/.circleci/config.yml b/.circleci/config.yml index 661a5c8..758474e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -53,6 +53,24 @@ jobs: command: | curl -s -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAX_TOKEN&suffix=tar.gz" -o GeoLite2-Country.tar.gz tar -xzf GeoLite2-Country.tar.gz --strip-components 1 --wildcards "*.mmdb" + + - run: + name: Download and Extract ARM Nebula Binary + command: | + curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-arm64.tar.gz + tar -xzvf nebula-linux-arm64.tar.gz + + - run: + name: Rename ARM Nebula Binary + command: | + mv nebula nebula-arm + mv nebula-cert nebula-cert-arm + + - run: + name: Download and Extract Nebula Binary + command: | + curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-amd64.tar.gz + tar -xzvf nebula-linux-amd64.tar.gz - run: name: Build UI diff --git a/.gitignore b/.gitignore index 6f546b6..d347d46 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,9 @@ todo.txt LICENCE tokens.json .vscode -GeoLite2-Country.mmdb \ No newline at end of file +GeoLite2-Country.mmdb +zz_test_config +nebula-arm +nebula-arm-cert +nebula +nebula-cert \ No newline at end of file diff --git a/build.sh b/build.sh index bd48565..57b3348 100644 --- a/build.sh +++ b/build.sh @@ -18,6 +18,7 @@ echo " ---- Build complete, copy assets ----" cp -r static build/ cp -r GeoLite2-Country.mmdb build/ +cp nebula-arm-cert nebula-cert nebula-arm nebula build/ cp -r Logo.png build/ mkdir build/images cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png diff --git a/changelog.md b/changelog.md index 4e3d8dc..d3d2ed4 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,12 @@ +<<<<<<< HEAD ## Version 0.9.20 - 0.9.21 - Add option to disable CORS hardening (with empty value) +======= +## Version 0.10.0 + - Added Constellation + - DNS Challenge is now used for all certificates when enabled +>>>>>>> b8a9e71 ([release] v0.10.0-unstable) ## Version 0.9.19 - Add country whitelist option to geoblocker - No countries blocked by default anymore diff --git a/client/src/api/constellation.tsx b/client/src/api/constellation.tsx new file mode 100644 index 0000000..717f78a --- /dev/null +++ b/client/src/api/constellation.tsx @@ -0,0 +1,25 @@ +import wrap from './wrap'; + +function list() { + return wrap(fetch('/cosmos/api/constellation/devices', { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + })) +} + +function addDevice(device) { + return wrap(fetch('/cosmos/api/constellation/devices', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(device), + })) +} + +export { + list, + addDevice, +}; \ No newline at end of file diff --git a/client/src/api/downloadButton.jsx b/client/src/api/downloadButton.jsx new file mode 100644 index 0000000..814e6a7 --- /dev/null +++ b/client/src/api/downloadButton.jsx @@ -0,0 +1,28 @@ +import { Button } from "@mui/material"; + +export const DownloadFile = ({ filename, content, label }) => { + const downloadFile = () => { + // Create a blob with the content + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + + // Create a link element + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = filename; + + // Append the link to the document (needed for Firefox) + document.body.appendChild(link); + + // Simulate a click to start the download + link.click(); + + // Cleanup the DOM by removing the link element + document.body.removeChild(link); + } + + return ( + + {label} + + ); +} \ No newline at end of file diff --git a/client/src/api/index.jsx b/client/src/api/index.jsx index 492bc78..3472e51 100644 --- a/client/src/api/index.jsx +++ b/client/src/api/index.jsx @@ -3,6 +3,7 @@ import * as _users from './users'; import * as _config from './config'; import * as _docker from './docker'; import * as _market from './market'; +import * as _constellation from './constellation'; import * as authDemo from './authentication.demo'; import * as usersDemo from './users.demo'; @@ -211,6 +212,7 @@ let users = _users; let config = _config; let docker = _docker; let market = _market; +let constellation = _constellation; if(isDemo) { auth = authDemo; @@ -232,6 +234,7 @@ export { config, docker, market, + constellation, getStatus, newInstall, isOnline, diff --git a/client/src/assets/images/icons/constellation.png b/client/src/assets/images/icons/constellation.png new file mode 100644 index 0000000..d507a44 Binary files /dev/null and b/client/src/assets/images/icons/constellation.png differ diff --git a/client/src/menu-items/pages.jsx b/client/src/menu-items/pages.jsx index 335fbe6..a2e4068 100644 --- a/client/src/menu-items/pages.jsx +++ b/client/src/menu-items/pages.jsx @@ -1,5 +1,6 @@ // assets import { ProfileOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons'; +import ConstellationIcon from '../assets/images/icons/constellation.png' // icons const icons = { @@ -7,7 +8,6 @@ const icons = { ProfileOutlined, SettingOutlined }; - // ==============================|| MENU ITEMS - EXTRA PAGES ||============================== // const pages = { @@ -29,6 +29,14 @@ const pages = { url: '/cosmos-ui/config-url', icon: icons.NodeExpandOutlined, }, + { + id: 'constellation', + title: 'Constellation', + type: 'item', + url: '/cosmos-ui/constellation', + icon: () => , + + }, { id: 'users', title: 'Users', diff --git a/client/src/pages/constellation/addDevice.jsx b/client/src/pages/constellation/addDevice.jsx new file mode 100644 index 0000000..85de900 --- /dev/null +++ b/client/src/pages/constellation/addDevice.jsx @@ -0,0 +1,147 @@ +// material-ui +import { Alert, Button, Stack, TextField } from '@mui/material'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import * as React from 'react'; +import { useState } from 'react'; +import ResponsiveButton from '../../components/responseiveButton'; +import { PlusCircleFilled } from '@ant-design/icons'; +import { Formik } from 'formik'; +import * as yup from 'yup'; +import * as API from '../../api'; +import { CosmosInputText, CosmosSelect } from '../config/users/formShortcuts'; +import { DownloadFile } from '../../api/downloadButton'; + +const AddDeviceModal = ({ config, isAdmin, refreshConfig, devices }) => { + const [openModal, setOpenModal] = useState(false); + const [isDone, setIsDone] = useState(null); + + return <> + setOpenModal(false)}> + { + return API.constellation.addDevice(values).then(({data}) => { + setIsDone(data); + refreshConfig(); + }).catch((err) => { + setErrors(err.response.data); + }); + }} + > + {(formik) => ( + + Manually Add Device + + {isDone ? + + + Device added successfully! + Download the private and public keys to your device along side the config and network certificate to connect: + + + + + + + + + + : + + Manually add a device to the constellation. It is recommended that you use the Cosmos app instead. Use this form to add another Nebula device manually + + + + + + + + + + + {formik.errors && formik.errors.length > 0 && + {formik.errors.map((err) => { + return {err} + })} + } + + + + + } + + + setOpenModal(false)}>Close + Add + + + + )} + + + + { + setIsDone(null); + setOpenModal(true); + }} + variant="contained" + startIcon={} + > + Manually Add Device + + >; +}; + +export default AddDeviceModal; diff --git a/client/src/pages/constellation/index.jsx b/client/src/pages/constellation/index.jsx new file mode 100644 index 0000000..d0550bb --- /dev/null +++ b/client/src/pages/constellation/index.jsx @@ -0,0 +1,81 @@ +import React from "react"; +import { useEffect, useState } from "react"; +import * as API from "../../api"; +import AddDeviceModal from "./addDevice"; +import PrettyTableView from "../../components/tableView/prettyTableView"; +import { DeleteButton } from "../../components/delete"; +import { CloudOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons"; +import IsLoggedIn from "../../isLoggedIn"; + +export const ConstellationIndex = () => { + const [isAdmin, setIsAdmin] = useState(false); + const [config, setConfig] = useState(null); + const [devices, setDevices] = useState(null); + + const refreshConfig = async () => { + let configAsync = await API.config.get(); + setConfig(configAsync.data); + setIsAdmin(configAsync.isAdmin); + setDevices((await API.constellation.list()).data || []); + }; + + useEffect(() => { + refreshConfig(); + }, []); + + const getIcon = (r) => { + if (r.deviceName.toLowerCase().includes("mobile") || r.deviceName.toLowerCase().includes("phone")) { + return + } + else if (r.deviceName.toLowerCase().includes("laptop") || r.deviceName.toLowerCase().includes("computer")) { + return + } else if (r.deviceName.toLowerCase().includes("desktop")) { + return + } else if (r.deviceName.toLowerCase().includes("tablet")) { + return + } else { + return + } + } + + return <> + + {devices && config && <> + r.deviceName} + buttons={[ + + ]} + columns={[ + { + title: '', + field: getIcon, + }, + { + title: 'Device Name', + field: (r) => {r.deviceName}, + }, + { + title: 'Owner', + field: (r) => {r.nickname}, + }, + { + title: 'Constellation IP', + screenMin: 'md', + field: (r) => r.ip, + }, + { + title: '', + clickable: true, + field: (r) => { + return { + alert("caca") + }}> + } + } + ]} + /> + >} + > +}; \ No newline at end of file diff --git a/client/src/routes/MainRoutes.jsx b/client/src/routes/MainRoutes.jsx index 503e179..5c67c26 100644 --- a/client/src/routes/MainRoutes.jsx +++ b/client/src/routes/MainRoutes.jsx @@ -15,6 +15,7 @@ import ContainerIndex from '../pages/servapps/containers'; import NewDockerServiceForm from '../pages/servapps/containers/newServiceForm'; import OpenIdList from '../pages/openid/openid-list'; import MarketPage from '../pages/market/listing'; +import { ConstellationIndex } from '../pages/constellation'; // render - dashboard @@ -44,6 +45,10 @@ const MainRoutes = { path: '/cosmos-ui/dashboard', element: }, + { + path: '/cosmos-ui/constellation', + element: + }, { path: '/cosmos-ui/servapps', element: diff --git a/client/src/utils/indexs.js b/client/src/utils/indexs.js index cf7a756..8caa9b5 100644 --- a/client/src/utils/indexs.js +++ b/client/src/utils/indexs.js @@ -1,3 +1,5 @@ +import { Button } from "@mui/material"; + export const randomString = (length) => { let text = ""; const possible = @@ -45,4 +47,4 @@ export const redirectToLocal = (url) => { throw new Error("URL must be local"); } window.location.href = url; -} \ No newline at end of file +} diff --git a/dockerfile b/dockerfile index ccbe859..e7949ce 100644 --- a/dockerfile +++ b/dockerfile @@ -29,7 +29,7 @@ WORKDIR /app COPY build/cosmos build/cosmos-arm64 ./ # Copy other resources -COPY build/cosmos_gray.png build/Logo.png build/GeoLite2-Country.mmdb build/meta.json ./ +COPY build/* ./ COPY static ./static # Run the respective binary based on the BINARY_NAME diff --git a/dockerfile.arm64 b/dockerfile.arm64 index 2a313e2..79b1f58 100644 --- a/dockerfile.arm64 +++ b/dockerfile.arm64 @@ -13,7 +13,8 @@ RUN apt-get update \ WORKDIR /app -COPY build/cosmos build/cosmos_gray.png build/Logo.png build/GeoLite2-Country.mmdb build/meta.json ./ + +COPY build/* ./ COPY static ./static CMD ["./cosmos"] diff --git a/package.json b/package.json index bcbcb59..aa4b7dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.9.21", + "version": "0.10.0-unstable", "description": "", "main": "test-server.js", "bugs": { @@ -63,13 +63,12 @@ "scripts": { "client": "vite", "client-build": "vite build --base=/cosmos-ui/", - "start": "env CONFIG_FILE=./config_dev.json EZ=UTC ACME_STAGING=true build/cosmos", + "start": "env COSMOS_CONFIG_FOLDER=/mnt/e/work/Cosmos-Server/zz_test_config/ CONFIG_FILE=./config_dev.json EZ=UTC ACME_STAGING=true build/cosmos", "build": "sh build.sh", "dev": "npm run build && npm run start", "dockerdevbuild": "sh build.sh && docker build -f dockerfile.local --tag cosmos-dev .", - "dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run -d -p 7200:443 -p 80:80 -p 443:443 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev", - "dockerdev": "npm run dockerdevbuild && npm run dockerdevrun", - "dockerdevclient": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun", + "dockerdevrun": "docker stop cosmos-dev; docker rm cosmos-dev; docker run --cap-add NET_ADMIN -d -p 7200:443 -p 80:80 -p 443:443 -p 4242:4242 -e DOCKER_HOST=tcp://host.docker.internal:2375 -e COSMOS_MONGODB=$MONGODB -e COSMOS_LOG_LEVEL=DEBUG -v /:/mnt/host --restart=unless-stopped -h cosmos-dev --name cosmos-dev cosmos-dev", + "dockerdev": "npm run client-build && npm run dockerdevbuild && npm run dockerdevrun", "demo": "vite build --base=/cosmos-ui/ --mode demo", "devdemo": "vite --mode demo" }, diff --git a/src/background.go b/src/background.go index fc5eda4..b096d0f 100644 --- a/src/background.go +++ b/src/background.go @@ -54,7 +54,7 @@ func UploadBackground(w http.ResponseWriter, req *http.Request) { } // create a new file in the config directory - dst, err := os.Create("/config/background" + ext) + dst, err := os.Create(utils.CONFIGFOLDER + "background" + ext) if err != nil { utils.HTTPError(w, "Error creating destination file", http.StatusInternalServerError, "FILE004") return @@ -99,7 +99,7 @@ func GetBackground(w http.ResponseWriter, req *http.Request) { if(req.Method == "GET") { // get the background image - bg, err := ioutil.ReadFile("/config/background." + ext) + bg, err := ioutil.ReadFile(utils.CONFIGFOLDER + "background." + ext) if err != nil { utils.HTTPError(w, "Error reading background image", http.StatusInternalServerError, "FILE003") return diff --git a/src/configapi/get.go b/src/configapi/get.go index fac06ea..7af53bc 100644 --- a/src/configapi/get.go +++ b/src/configapi/get.go @@ -42,6 +42,7 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) { "data": config, "updates": utils.UpdateAvailable, "hostname": os.Getenv("HOSTNAME"), + "isAdmin": isAdmin, }) } else { utils.Error("SettingGet: Method not allowed" + req.Method, nil) diff --git a/src/constellation/api_devices_create.go b/src/constellation/api_devices_create.go new file mode 100644 index 0000000..014132c --- /dev/null +++ b/src/constellation/api_devices_create.go @@ -0,0 +1,129 @@ +package constellation + +import ( + "net/http" + "encoding/json" + "go.mongodb.org/mongo-driver/mongo" + + "github.com/azukaar/cosmos-server/src/utils" +) + +type DeviceCreateRequestJSON struct { + Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"` + DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"` + IP string `json:"ip",validate:"required,ipv4"` + PublicKey string `json:"publicKey",omitempty` +} + +func DeviceCreate(w http.ResponseWriter, req *http.Request) { + + if(req.Method == "POST") { + var request DeviceCreateRequestJSON + err1 := json.NewDecoder(req.Body).Decode(&request) + if err1 != nil { + utils.Error("ConstellationDeviceCreation: Invalid User Request", err1) + utils.HTTPError(w, "Device Creation Error", + http.StatusInternalServerError, "DC001") + return + } + + errV := utils.Validate.Struct(request) + if errV != nil { + utils.Error("DeviceCreation: Invalid User Request", errV) + utils.HTTPError(w, "Device Creation Error: " + errV.Error(), + http.StatusInternalServerError, "DC002") + return + } + + nickname := utils.Sanitize(request.Nickname) + deviceName := utils.Sanitize(request.DeviceName) + + if utils.AdminOrItselfOnly(w, req, nickname) != nil { + return + } + + c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices") + 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, + }).Decode(&device) + + if err2 == mongo.ErrNoDocuments { + cert, key, err := generateNebulaCert(deviceName, request.IP, false) + + if err != nil { + utils.Error("DeviceCreation: Error while creating Device", err) + utils.HTTPError(w, "Device Creation Error: " + err.Error(), + http.StatusInternalServerError, "DC001") + return + } + + _, err3 := c.InsertOne(nil, map[string]interface{}{ + "Nickname": nickname, + "DeviceName": deviceName, + "PublicKey": cert, + "PrivateKey": key, + "IP": request.IP, + }) + + if err3 != nil { + utils.Error("DeviceCreation: Error while creating Device", err3) + utils.HTTPError(w, "Device Creation Error: " + err.Error(), + http.StatusInternalServerError, "DC004") + return + } + + // read configYml from config/nebula.yml + configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml") + if err != nil { + utils.Error("DeviceCreation: Error while reading config", err) + utils.HTTPError(w, "Device Creation Error: " + err.Error(), + http.StatusInternalServerError, "DC005") + 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 + } + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": map[string]interface{}{ + "Nickname": nickname, + "DeviceName": deviceName, + "PublicKey": cert, + "PrivateKey": key, + "IP": request.IP, + "Config": configYml, + "CA": capki, + }, + }) + } 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 + } + } else { + utils.Error("DeviceCreation: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/constellation/api_devices_index.go b/src/constellation/api_devices_index.go new file mode 100644 index 0000000..38b2a12 --- /dev/null +++ b/src/constellation/api_devices_index.go @@ -0,0 +1,18 @@ +package constellation + +import ( + "net/http" + "github.com/azukaar/cosmos-server/src/utils" +) + +func ConstellationAPIDevices(w http.ResponseWriter, req *http.Request) { + if (req.Method == "GET") { + DeviceList(w, req) + } else if (req.Method == "POST") { + DeviceCreate(w, req) + } else { + utils.Error("UserRoute: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/constellation/api_devices_list.go b/src/constellation/api_devices_list.go new file mode 100644 index 0000000..c2efc0f --- /dev/null +++ b/src/constellation/api_devices_list.go @@ -0,0 +1,83 @@ +package constellation + +import ( + "net/http" + "encoding/json" + + + "github.com/azukaar/cosmos-server/src/utils" +) + +func DeviceList(w http.ResponseWriter, req *http.Request) { + // Check for GET method + if req.Method != "GET" { + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002") + return + } + + if utils.LoggedInOnly(w, req) != nil { + return + } + + isAdmin := utils.IsAdmin(req) + + // Connect to the collection + c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } + + var devices []utils.Device + + // Check if user is an admin + if isAdmin { + // If admin, get all devices + cursor, err := c.Find(nil, map[string]interface{}{}) + 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") + return + } + + // Remove the private key from the response + for i := range devices { + devices[i].PrivateKey = "" + } + } else { + // If not admin, get user's devices based on their nickname + nickname := req.Header.Get("x-cosmos-user") + cursor, err := c.Find(nil, map[string]interface{}{"Nickname": nickname}) + 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") + return + } + + // Remove the private key from the response + for i := range devices { + devices[i].PrivateKey = "" + } + } + + // Respond with the list of devices + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "data": devices, + }) +} diff --git a/src/constellation/index.go b/src/constellation/index.go new file mode 100644 index 0000000..07426ba --- /dev/null +++ b/src/constellation/index.go @@ -0,0 +1,42 @@ +package constellation + +import ( + "github.com/azukaar/cosmos-server/src/utils" + "os" +) + +func Init() { + // if Constellation is enabled + if utils.GetMainConfig().ConstellationConfig.Enabled { + InitConfig() + + utils.Log("Initializing Constellation module...") + + // 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 + generateNebulaCACert("Cosmos - " + utils.GetMainConfig().HTTPConfig.Hostname) + } + + // 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 + generateNebulaCert("cosmos", "192.168.201.0/24", true) + } + + // export nebula.yml + utils.Log("Constellation: exporting nebula.yml...") + ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml") + + // start nebula + utils.Log("Constellation: starting nebula...") + err := startNebulaInBackground() + if err != nil { + utils.Error("Constellation: error while starting nebula", err) + } + + utils.Log("Constellation module initialized") + } +} \ No newline at end of file diff --git a/src/constellation/nebula.go b/src/constellation/nebula.go new file mode 100644 index 0000000..227b331 --- /dev/null +++ b/src/constellation/nebula.go @@ -0,0 +1,283 @@ +package constellation + +import ( + "github.com/azukaar/cosmos-server/src/utils" + "os/exec" + "os" + "fmt" + "errors" + "runtime" + "sync" + "gopkg.in/yaml.v2" + "strings" + "io/ioutil" + "strconv" +) + +var ( + process *exec.Cmd + processMux sync.Mutex +) + +func binaryToRun() string { + if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { + return "./nebula-arm" + } + return "./nebula" +} + +func startNebulaInBackground() error { + processMux.Lock() + defer processMux.Unlock() + + if process != nil { + return errors.New("nebula is already running") + } + + process = exec.Command(binaryToRun(), "-config", utils.CONFIGFOLDER + "nebula.yml") + + process.Stderr = os.Stderr + + if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG { + process.Stdout = os.Stdout + } else { + process.Stdout = nil + } + + // Start the process in the background + if err := process.Start(); err != nil { + return err + } + + utils.Log(fmt.Sprintf("%s started with PID %d\n", binaryToRun(), process.Process.Pid)) + return nil +} + +func stop() error { + processMux.Lock() + defer processMux.Unlock() + + if process == nil { + return errors.New("nebula is not running") + } + + if err := process.Process.Kill(); err != nil { + return err + } + process = nil + utils.Log("Stopped nebula.") + return nil +} + +func restart() error { + if err := stop(); err != nil { + return err + } + return startNebulaInBackground() +} + +func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath string) error { + // Combine defaultConfig and overwriteConfig + finalConfig := NebulaDefaultConfig + + finalConfig.StaticHostMap = map[string][]string{ + "192.168.201.0": []string{utils.GetMainConfig().HTTPConfig.Hostname + ":4242"}, + } + + // Marshal the combined config to YAML + yamlData, err := yaml.Marshal(finalConfig) + if err != nil { + return err + } + + // Write YAML data to the specified file + yamlFile, err := os.Create(outputPath) + if err != nil { + return err + } + defer yamlFile.Close() + + _, err = yamlFile.Write(yamlData) + if err != nil { + return err + } + + return nil +} + +func getYAMLClientConfig(name, configPath string) (string, error) { + utils.Log("Exporting YAML config for " + name + " with file " + configPath) + + // Read the YAML config file + yamlData, err := ioutil.ReadFile(configPath) + if err != nil { + return "", err + } + + // Unmarshal the YAML data into a map interface + var configMap map[string]interface{} + err = yaml.Unmarshal(yamlData, &configMap) + if err != nil { + return "", err + } + + // set lightHouse to false + if lighthouseMap, ok := configMap["lighthouse"].(map[interface{}]interface{}); ok { + lighthouseMap["am_lighthouse"] = false + + lighthouseMap["hosts"] = []string{ + "192.168.201.0", + } + } else { + return "", errors.New("lighthouse not found in nebula.yml") + } + + if pkiMap, ok := configMap["pki"].(map[interface{}]interface{}); ok { + pkiMap["ca"] = "ca.crt" + pkiMap["cert"] = name + ".crt" + pkiMap["key"] = name + ".key" + } else { + return "", errors.New("pki not found in nebula.yml") + } + + // export configMap as YML + yamlData, err = yaml.Marshal(configMap) + if err != nil { + return "", err + } + + return string(yamlData), nil +} + +func getCApki() (string, error) { + // read config/ca.crt + caCrt, err := ioutil.ReadFile(utils.CONFIGFOLDER + "ca.crt") + if err != nil { + return "", err + } + + return string(caCrt), nil +} + +func killAllNebulaInstances() error { + processMux.Lock() + defer processMux.Unlock() + + cmd := exec.Command("ps", "-e", "-o", "pid,command") + output, err := cmd.CombinedOutput() + if err != nil { + return err + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, binaryToRun()) { + fields := strings.Fields(line) + if len(fields) > 1 { + pid := fields[0] + pidInt, _ := strconv.Atoi(pid) + process, err := os.FindProcess(pidInt) + if err != nil { + return err + } + err = process.Kill() + if err != nil { + return err + } + utils.Log(fmt.Sprintf("Killed Nebula instance with PID %s\n", pid)) + } + } + } + + return nil +} + +func generateNebulaCert(name, ip string, saveToFile bool) (string, string, error) { + // Run the nebula-cert command + cmd := exec.Command(binaryToRun() + "-cert", + "sign", + "-ca-crt", utils.CONFIGFOLDER + "ca.crt", + "-ca-key", utils.CONFIGFOLDER + "ca.key", + "-name", name, + "-ip", ip, + ) + utils.Debug(cmd.String()) + + cmd.Stderr = os.Stderr + + if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG { + cmd.Stdout = os.Stdout + } else { + cmd.Stdout = nil + } + + cmd.Run() + + if cmd.ProcessState.ExitCode() != 0 { + return "", "", fmt.Errorf("nebula-cert exited with an error, check the Cosmos logs") + } + + // Read the generated certificate and key files + certPath := fmt.Sprintf("./%s.crt", name) + keyPath := fmt.Sprintf("./%s.key", name) + + utils.Debug("Reading certificate from " + certPath) + utils.Debug("Reading key from " + keyPath) + + certContent, errCert := ioutil.ReadFile(certPath) + if errCert != nil { + return "", "", fmt.Errorf("failed to read certificate file: %s", errCert) + } + + keyContent, errKey := ioutil.ReadFile(keyPath) + if errKey != nil { + return "", "", fmt.Errorf("failed to read key file: %s", errKey) + } + + if saveToFile { + cmd = exec.Command("mv", certPath, utils.CONFIGFOLDER + name + ".crt") + utils.Debug(cmd.String()) + cmd.Run() + cmd = exec.Command("mv", keyPath, utils.CONFIGFOLDER + name + ".key") + utils.Debug(cmd.String()) + cmd.Run() + } else { + // Delete the generated certificate and key files + if err := os.Remove(certPath); err != nil { + return "", "", fmt.Errorf("failed to delete certificate file: %s", err) + } + + if err := os.Remove(keyPath); err != nil { + return "", "", fmt.Errorf("failed to delete key file: %s", err) + } + } + + return string(certContent), string(keyContent), nil +} + +func generateNebulaCACert(name string) (error) { + // Run the nebula-cert command to generate CA certificate and key + cmd := exec.Command(binaryToRun() + "-cert", "ca", "-name", "\""+name+"\"") + + utils.Debug(cmd.String()) + + cmd.Stderr = os.Stderr + + if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG { + cmd.Stdout = os.Stdout + } else { + cmd.Stdout = nil + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("nebula-cert error: %s", err) + } + + // copy to /config/ca.* + cmd = exec.Command("mv", "./ca.crt", utils.CONFIGFOLDER + "ca.crt") + cmd.Run() + cmd = exec.Command("mv", "./ca.key", utils.CONFIGFOLDER + "ca.key") + cmd.Run() + + return nil +} \ No newline at end of file diff --git a/src/constellation/nebula_default.go b/src/constellation/nebula_default.go new file mode 100644 index 0000000..1264ff7 --- /dev/null +++ b/src/constellation/nebula_default.go @@ -0,0 +1,95 @@ +package constellation + +import ( + "github.com/azukaar/cosmos-server/src/utils" +) + +var NebulaDefaultConfig utils.NebulaConfig + +func InitConfig() { + NebulaDefaultConfig = utils.NebulaConfig { + PKI: struct { + CA string `yaml:"ca"` + Cert string `yaml:"cert"` + Key string `yaml:"key"` + }{ + CA: utils.CONFIGFOLDER + "ca.crt", + Cert: utils.CONFIGFOLDER + "cosmos.crt", + Key: utils.CONFIGFOLDER + "cosmos.key", + }, + StaticHostMap: map[string][]string{ + + }, + Lighthouse: struct { + AMLighthouse bool `yaml:"am_lighthouse"` + Interval int `yaml:"interval"` + Hosts []string `yaml:"hosts"` + }{ + AMLighthouse: true, + Interval: 60, + Hosts: []string{}, + }, + Listen: struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + }{ + Host: "0.0.0.0", + Port: 4242, + }, + Punchy: struct { + Punch bool `yaml:"punch"` + }{ + Punch: true, + }, + Relay: struct { + AMRelay bool `yaml:"am_relay"` + UseRelays bool `yaml:"use_relays"` + }{ + AMRelay: false, + UseRelays: true, + }, + TUN: struct { + Disabled bool `yaml:"disabled"` + Dev string `yaml:"dev"` + DropLocalBroadcast bool `yaml:"drop_local_broadcast"` + DropMulticast bool `yaml:"drop_multicast"` + TxQueue int `yaml:"tx_queue"` + MTU int `yaml:"mtu"` + Routes []string `yaml:"routes"` + UnsafeRoutes []string `yaml:"unsafe_routes"` + }{ + Disabled: false, + Dev: "nebula1", + DropLocalBroadcast: false, + DropMulticast: false, + TxQueue: 500, + MTU: 1300, + Routes: nil, + UnsafeRoutes: nil, + }, + Logging: struct { + Level string `yaml:"level"` + Format string `yaml:"format"` + }{ + Level: "info", + Format: "text", + }, + Firewall: struct { + OutboundAction string `yaml:"outbound_action"` + InboundAction string `yaml:"inbound_action"` + Conntrack utils.NebulaConntrackConfig `yaml:"conntrack"` + Outbound []utils.NebulaFirewallRule `yaml:"outbound"` + Inbound []utils.NebulaFirewallRule `yaml:"inbound"` + }{ + OutboundAction: "drop", + InboundAction: "drop", + Conntrack: utils.NebulaConntrackConfig{ + TCPTimeout: "12m", + UDPTimeout: "3m", + DefaultTimeout: "10m", + }, + Outbound: nil, + Inbound: nil, + }, + } +} \ No newline at end of file diff --git a/src/httpServer.go b/src/httpServer.go index 1e9c2c9..49e7713 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -9,6 +9,7 @@ import ( "github.com/azukaar/cosmos-server/src/docker" "github.com/azukaar/cosmos-server/src/authorizationserver" "github.com/azukaar/cosmos-server/src/market" + "github.com/azukaar/cosmos-server/src/constellation" "github.com/gorilla/mux" "strconv" "time" @@ -331,6 +332,7 @@ func InitServer() *mux.Router { srapi.HandleFunc("/api/background", UploadBackground) srapi.HandleFunc("/api/background/{ext}", GetBackground) + srapi.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices) if(!config.HTTPConfig.AcceptAllInsecureHostname) { srapi.Use(utils.EnsureHostname) diff --git a/src/index.go b/src/index.go index bc1a1b8..4e9fc2d 100644 --- a/src/index.go +++ b/src/index.go @@ -9,6 +9,7 @@ import ( "github.com/azukaar/cosmos-server/src/utils" "github.com/azukaar/cosmos-server/src/authorizationserver" "github.com/azukaar/cosmos-server/src/market" + "github.com/azukaar/cosmos-server/src/constellation" ) func main() { @@ -44,5 +45,7 @@ func main() { authorizationserver.Init() + constellation.Init() + StartServer() } diff --git a/src/user/create.go b/src/user/create.go index 9e6b38b..b2e2f1c 100644 --- a/src/user/create.go +++ b/src/user/create.go @@ -2,12 +2,9 @@ package user import ( "net/http" - // "io" - // "os" "encoding/json" "go.mongodb.org/mongo-driver/mongo" "time" - // "golang.org/x/crypto/bcrypt" "github.com/azukaar/cosmos-server/src/utils" ) diff --git a/src/utils/certificates.go b/src/utils/certificates.go index 9ea6dd0..5e19a5c 100644 --- a/src/utils/certificates.go +++ b/src/utils/certificates.go @@ -181,20 +181,20 @@ func DoLetsEncrypt() (string, string) { } err = client.Challenge.SetDNS01Provider(provider) - } + } else { + err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", config.HTTPConfig.HTTPPort)) + if err != nil { + Error("LETSENCRYPT_HTTP01", err) + LetsEncryptErrors = append(LetsEncryptErrors, err.Error()) + return "", "" + } - err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", config.HTTPConfig.HTTPPort)) - if err != nil { - Error("LETSENCRYPT_HTTP01", err) - LetsEncryptErrors = append(LetsEncryptErrors, err.Error()) - return "", "" - } - - err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", config.HTTPConfig.HTTPSPort)) - if err != nil { - Error("LETSENCRYPT_TLS01", err) - LetsEncryptErrors = append(LetsEncryptErrors, err.Error()) - return "", "" + err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", config.HTTPConfig.HTTPSPort)) + if err != nil { + Error("LETSENCRYPT_TLS01", err) + LetsEncryptErrors = append(LetsEncryptErrors, err.Error()) + return "", "" + } } // New users will need to register diff --git a/src/utils/types.go b/src/utils/types.go index 1f6d9f3..7b1d85d 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -90,6 +90,7 @@ type Config struct { MarketConfig MarketConfig HomepageConfig HomepageConfig ThemeConfig ThemeConfig + ConstellationConfig ConstellationConfig } type HomepageConfig struct { @@ -205,4 +206,84 @@ type MarketConfig struct { type MarketSource struct { Name string Url string -} \ No newline at end of file +} + +type ConstellationConfig struct { + Enabled bool + NebulaConfig NebulaConfig +} + +type NebulaFirewallRule struct { + Port string `yaml:"port"` + Proto string `yaml:"proto"` + Host string `yaml:"host"` + Groups []string `yaml:"groups,omitempty"omitempty"` +} + +type NebulaConntrackConfig struct { + TCPTimeout string `yaml:"tcp_timeout"` + UDPTimeout string `yaml:"udp_timeout"` + DefaultTimeout string `yaml:"default_timeout"` +} + +type NebulaConfig struct { + PKI struct { + CA string `yaml:"ca"` + Cert string `yaml:"cert"` + Key string `yaml:"key"` + } `yaml:"pki"` + + StaticHostMap map[string][]string `yaml:"static_host_map"` + + Lighthouse struct { + AMLighthouse bool `yaml:"am_lighthouse"` + Interval int `yaml:"interval"` + Hosts []string `yaml:"hosts"` + } `yaml:"lighthouse"` + + Listen struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + } `yaml:"listen"` + + Punchy struct { + Punch bool `yaml:"punch"` + } `yaml:"punchy"` + + Relay struct { + AMRelay bool `yaml:"am_relay"` + UseRelays bool `yaml:"use_relays"` + } `yaml:"relay"` + + TUN struct { + Disabled bool `yaml:"disabled"` + Dev string `yaml:"dev"` + DropLocalBroadcast bool `yaml:"drop_local_broadcast"` + DropMulticast bool `yaml:"drop_multicast"` + TxQueue int `yaml:"tx_queue"` + MTU int `yaml:"mtu"` + Routes []string `yaml:"routes"` + UnsafeRoutes []string `yaml:"unsafe_routes"` + } `yaml:"tun"` + + Logging struct { + Level string `yaml:"level"` + Format string `yaml:"format"` + } `yaml:"logging"` + + Firewall struct { + OutboundAction string `yaml:"outbound_action"` + InboundAction string `yaml:"inbound_action"` + Conntrack NebulaConntrackConfig `yaml:"conntrack"` + Outbound []NebulaFirewallRule `yaml:"outbound"` + Inbound []NebulaFirewallRule `yaml:"inbound"` + } `yaml:"firewall"` +} + +type Device struct { + DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"` + Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"` + PublicKey string `json:"publicKey",omitempty` + PrivateKey string `json:"privateKey",omitempty` + IP string `json:"ip",validate:"required,ipv4"` +} diff --git a/src/utils/utils.go b/src/utils/utils.go index 6177a0c..a64f1d7 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -37,6 +37,8 @@ var ReBootstrapContainer func(string) error var LetsEncryptErrors = []string{} +var CONFIGFOLDER = "/config/" + var DefaultConfig = Config{ LoggingLevel: "INFO", NewInstall: true, @@ -193,6 +195,10 @@ func LoadBaseMainConfig(config Config) { if os.Getenv("COSMOS_SERVER_COUNTRY") != "" { MainConfig.ServerCountry = os.Getenv("COSMOS_SERVER_COUNTRY") } + if os.Getenv("COSMOS_CONFIG_FOLDER") != "" { + Log("Overwriting config folder with " + os.Getenv("COSMOS_CONFIG_FOLDER")) + CONFIGFOLDER = os.Getenv("COSMOS_CONFIG_FOLDER") + } if MainConfig.DockerConfig.DefaultDataPath == "" { MainConfig.DockerConfig.DefaultDataPath = "/usr" @@ -219,7 +225,7 @@ func GetConfigFileName() string { configFile := os.Getenv("CONFIG_FILE") if configFile == "" { - configFile = "/config/cosmos.config.json" + configFile = CONFIGFOLDER + "cosmos.config.json" } return configFile
+ Device added successfully! + Download the private and public keys to your device along side the config and network certificate to connect: +
Manually add a device to the constellation. It is recommended that you use the Cosmos app instead. Use this form to add another Nebula device manually