[release] v0.10.0-unstable15

This commit is contained in:
Yann Stepienik 2023-09-22 18:10:43 +01:00
parent ef37940742
commit a6b96bc42a
19 changed files with 570 additions and 99 deletions

View file

@ -6,7 +6,11 @@
## Version 0.10.0
- Added Constellation
- DNS Challenge is now used for all certificates when enabled
>>>>>>> b8a9e71 ([release] v0.10.0-unstable)
- Rework headers for better compatibility
## Version 0.9.20 - 0.9.21
- Add option to disable CORS hardening (with empty value)
## Version 0.9.19
- Add country whitelist option to geoblocker
- No countries blocked by default anymore

View file

@ -28,6 +28,16 @@ function restart() {
}))
}
function reset() {
return wrap(fetch('/cosmos/api/constellation/reset', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function getConfig() {
return wrap(fetch('/cosmos/api/constellation/config', {
method: 'GET',
@ -46,10 +56,22 @@ function getLogs() {
}))
}
function connect(file) {
return wrap(fetch('/cosmos/api/constellation/connect', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(file),
}))
}
export {
list,
addDevice,
restart,
getConfig,
getLogs,
reset,
connect,
};

View file

@ -0,0 +1,48 @@
// material-ui
import { LoadingButton } from '@mui/lab';
import { Button } 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 { useEffect, useState } from 'react';
const ConfirmModal = ({ callback, label, content }) => {
const [openModal, setOpenModal] = useState(false);
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText>
{content}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => {
setOpenModal(false);
}}>Cancel</Button>
<LoadingButton
onClick={() => {
callback();
setOpenModal(false);
}}>Confirm</LoadingButton>
</DialogActions>
</Dialog>
<Button
disableElevation
variant="outlined"
color="warning"
onClick={() => {
setOpenModal(true);
}}
>
{label}
</Button>
</>
};
export default ConfirmModal;

View file

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from '@mui/material';
import { UploadOutlined } from '@ant-design/icons';
export default function UploadButtons({OnChange, accept, label}) {
export default function UploadButtons({OnChange, accept, label, variant, fullWidth, size}) {
return (
<div>
<input
@ -14,7 +14,8 @@ export default function UploadButtons({OnChange, accept, label}) {
onChange={OnChange}
/>
<label htmlFor="contained-button-file">
<Button variant="contained" component="span" startIcon={<UploadOutlined />}>
<Button variant={variant || "contained"} component="span"
fullWidth={fullWidth} startIcon={<UploadOutlined />}>
{label || 'Upload'}
</Button>
</label>

View file

@ -12,10 +12,67 @@ import { PlusCircleFilled } from '@ant-design/icons';
import { Formik } from 'formik';
import * as yup from 'yup';
import * as API from '../../api';
import { CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
import { DownloadFile } from '../../api/downloadButton';
import QRCode from 'qrcode';
const getDocker = (data, isCompose) => {
let lighthouses = '';
for (let i = 0; i < data.LighthousesList.length; i++) {
const l = data.LighthousesList[i];
lighthouses += l.publicHostname + ";" + l.ip + ":" + l.port + ";" + l.isRelay + ",";
}
let containerName = "cosmos-constellation-lighthouse";
let imageName = "cosmos-constellation-lighthouse:latest";
let volPath = "/var/lib/cosmos-constellation";
if (isCompose) {
return `
version: "3.8"
services:
${containerName}:
image: ${imageName}
container_name: ${containerName}
restart: unless-stopped
network_mode: bridge
ports:
- "${data.Port}:4242"
volumes:
- ${volPath}:/config
environment:
- CA=${JSON.stringify(data.CA)}
- CERT=${JSON.stringify(data.PrivateKey)}
- KEY=${JSON.stringify(data.PublicKey)}
- LIGHTHOUSES=${lighthouses}
- PUBLIC_HOSTNAME=${data.PublicHostname}
- IS_RELAY=${data.IsRelay}
- IP=${data.IP}
`;
} else {
return `
docker run -d \\
--name ${containerName} \\
--restart unless-stopped \\
--network bridge \\
-v ${volPath}:/config \\
-e CA=${JSON.stringify(data.CA)} \\
-e CERT=${JSON.stringify(data.PrivateKey)} \\
-e KEY=${JSON.stringify(data.PublicKey)} \\
-e LIGHTHOUSES=${lighthouses} \\
-e PUBLIC_HOSTNAME=${data.PublicHostname} \\
-e IS_RELAY=${data.IsRelay} \\
-e IP=${data.IP} \\
-p ${data.Port}:4242 \\
${imageName}
`;
}
}
const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
const [openModal, setOpenModal] = useState(false);
const [isDone, setIsDone] = useState(null);
@ -63,12 +120,18 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
deviceName: '',
ip: firstIP,
publicKey: '',
Port: "4242",
PublicHostname: '',
IsRelay: true,
isLighthouse: false,
}}
validationSchema={yup.object({
})}
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
if(values.isLighthouse) values.nickname = null;
return API.constellation.addDevice(values).then(({data}) => {
setIsDone(data);
refreshConfig();
@ -85,52 +148,55 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
{isDone ? <DialogContent>
<DialogContentText>
<p>
Device added successfully!
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:
</p>
<Stack spacing={2} direction={"column"}>
<CosmosFormDivider title={"QR Code"} />
<div style={{textAlign: 'center'}}>
<canvas style={{borderRadius: '15px'}} ref={canvasRef} />
</div>
{/* <CosmosFormDivider title={"Cosmos Client (File)"} />
<DownloadFile
filename={isDone.DeviceName + `.constellation`}
content={JSON.stringify(isDone, null, 2)}
label={"Download " + isDone.DeviceName + `.constellation`}
/> */}
{/* {isDone.isLighthouse ? <>
<CosmosFormDivider title={"Docker"} />
<TextField
fullWidth
multiline
value={getDocker(isDone, false)}
variant="outlined"
size="small"
disabled
/>
<CosmosFormDivider title={"File (Docker-Compose)"} />
<DownloadFile
filename={`docker-compose.yml`}
content={getDocker(isDone, true)}
label={"Download docker-compose.yml"}
/>
</> : <> */}
<CosmosFormDivider title={"QR Code"} />
<div style={{textAlign: 'center'}}>
<canvas style={{borderRadius: '15px'}} ref={canvasRef} />
</div>
{/* </>} */}
<CosmosFormDivider title={"File"} />
<DownloadFile
filename={`constellation.yml`}
content={isDone.Config}
label={"Download constellation.yml"}
/>
{/* <DownloadFile
filename={isDone.DeviceName + `.key`}
content={isDone.PublicKey}
label={"Download " + isDone.DeviceName + `.key`}
/>
<DownloadFile
filename={isDone.DeviceName + `.crt`}
content={isDone.PrivateKey}
label={"Download " + isDone.DeviceName + `.crt`}
/>
<DownloadFile
filename={`ca.crt`}
content={isDone.CA}
label={"Download ca.crt"}
/> */}
</Stack>
</DialogContentText>
</DialogContent> : <DialogContent>
<DialogContentText>
<p>Add a device to the constellation using either the Cosmos or Nebula client</p>
<p>Add a Device to the constellation using either the Cosmos or Nebula client</p>
<div>
<Stack spacing={2} style={{}}>
<CosmosCheckbox
name="isLighthouse"
label="Lighthouse"
formik={formik}
/>
{!formik.values.isLighthouse &&
<CosmosSelect
name="nickname"
label="Owner"
@ -141,7 +207,7 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
return [u.nickname, u.nickname]
})
}
/>
/>}
<CosmosInputText
name="deviceName"
@ -155,12 +221,33 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
formik={formik}
/>
{/* <CosmosInputText
name="Port"
label="VPN Port (default: 4242)"
formik={formik}
/> */}
<CosmosInputText
multiline
name="publicKey"
label="Public Key (Optional)"
formik={formik}
/>
{formik.values.isLighthouse && <>
<CosmosFormDivider title={"Lighthouse Setup"} />
<CosmosInputText
name="PublicHostname"
label="Public Hostname"
formik={formik}
/>
<CosmosCheckbox
name="IsRelay"
label="Can Relay Traffic"
formik={formik}
/>
</>}
<div>
{formik.errors && formik.errors.length > 0 && <Stack spacing={2} direction={"column"}>
<Alert severity="error">{formik.errors.map((err) => {
@ -189,7 +276,9 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
setIsDone(null);
setOpenModal(true);
}}
variant="contained"
variant={
"contained"
}
startIcon={<PlusCircleFilled />}
>
Add Device

View file

@ -4,14 +4,26 @@ 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 { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
import IsLoggedIn from "../../isLoggedIn";
import { Button, CircularProgress, Stack } from "@mui/material";
import { CosmosCheckbox, CosmosFormDivider } from "../config/users/formShortcuts";
import { Alert, Button, CircularProgress, Stack } from "@mui/material";
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
import MainCard from "../../components/MainCard";
import { Formik } from "formik";
import { LoadingButton } from "@mui/lab";
import ApiModal from "../../components/apiModal";
import { isDomain } from "../../utils/indexs";
import ConfirmModal from "../../components/confirmModal";
import UploadButtons from "../../components/fileUpload";
const getDefaultConstellationHostname = (config) => {
// if domain is set, use it
if(isDomain(config.HTTPConfig.Hostname)) {
return "vpn." + config.HTTPConfig.Hostname;
} else {
return config.HTTPConfig.Hostname;
}
}
export const ConstellationIndex = () => {
const [isAdmin, setIsAdmin] = useState(false);
@ -41,6 +53,8 @@ export const ConstellationIndex = () => {
return <DesktopOutlined />
} else if (r.deviceName.toLowerCase().includes("tablet")) {
return <TabletOutlined />
} else if (r.deviceName.toLowerCase().includes("lighthouse") || r.deviceName.toLowerCase().includes("server")) {
return <CompassOutlined />
} else {
return <CloudOutlined />
}
@ -53,22 +67,30 @@ export const ConstellationIndex = () => {
<div>
<MainCard title={"Constellation Setup"} content={config.constellationIP}>
<Stack spacing={2}>
{config.ConstellationConfig.Enabled && config.ConstellationConfig.SlaveMode && <>
<Alert severity="info">
You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices.
</Alert>
</>}
<Formik
initialValues={{
Enabled: config.ConstellationConfig.Enabled,
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
getDefaultConstellationHostname(config)
}}
onSubmit={(values) => {
let newConfig = { ...config };
newConfig.ConstellationConfig.Enabled = values.Enabled;
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
return API.config.set(newConfig);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
<Stack spacing={2} direction="row">
{formik.values.Enabled && <Stack spacing={2} direction="row">
<Button
disableElevation
variant="outlined"
@ -77,14 +99,40 @@ export const ConstellationIndex = () => {
await API.constellation.restart();
}}
>
Restart Nebula
Restart VPN Service
</Button>
<ApiModal callback={API.constellation.getLogs} label={"Show Nebula logs"} />
<ApiModal callback={API.constellation.getConfig} label={"Render Nebula Config"} />
</Stack>
<ApiModal callback={API.constellation.getLogs} label={"Show VPN logs"} />
<ApiModal callback={API.constellation.getConfig} label={"Show VPN Config"} />
<ConfirmModal
variant="outlined"
color="warning"
label={"Reset Network"}
content={"This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone."}
callback={async () => {
await API.constellation.reset();
refreshConfig();
}}
/>
</Stack>}
<CosmosCheckbox formik={formik} name="Enabled" label="Constellation Enabled" />
<CosmosCheckbox formik={formik} name="IsRelay" label="Relay requests via this Node" />
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
{formik.values.Enabled && <>
<CosmosCheckbox formik={formik} name="IsRelay" label="Relay requests via this Node" />
<Alert severity="info">This is your Constellation hostname, that you will use to connect. If you are using a domain name, this needs to be different from your server's hostname. Whatever the domain you choose, it is very important that you make sure there is a A entry in your domain DNS pointing to this server. <strong>If you change this value, you will need to reset your network and reconnect all the clients!</strong></Alert>
<CosmosInputText formik={formik} name="ConstellationHostname" label="Constellation Hostname" />
</>}
</>}
<UploadButtons
accept=".yml,.yaml"
label={"Upload Nebula Config"}
variant="outlined"
fullWidth
OnChange={async (e) => {
let file = e.target.files[0];
await API.constellation.connect(file);
refreshConfig();
}}
/>
<LoadingButton
disableElevation
loading={formik.isSubmitting}
@ -101,12 +149,13 @@ export const ConstellationIndex = () => {
</Stack>
</MainCard>
</div>
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
<CosmosFormDivider title={"Devices"} />
<PrettyTableView
data={devices}
getKey={(r) => r.deviceName}
buttons={[
<AddDeviceModal isAdmin={isAdmin} users={users} config={config} refreshConfig={refreshConfig} devices={devices} />
<AddDeviceModal isAdmin={isAdmin} users={users} config={config} refreshConfig={refreshConfig} devices={devices} />,
]}
columns={[
{
@ -121,6 +170,10 @@ export const ConstellationIndex = () => {
title: 'Owner',
field: (r) => <strong>{r.nickname}</strong>,
},
{
title: 'Type',
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
},
{
title: 'Constellation IP',
screenMin: 'md',
@ -137,6 +190,7 @@ export const ConstellationIndex = () => {
}
]}
/>
</>}
</Stack>
</> : <center>
<CircularProgress color="inherit" size={20} />

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.10.0-unstable14",
"version": "0.10.0-unstable15",
"description": "",
"main": "test-server.js",
"bugs": {

View file

@ -9,10 +9,18 @@ import (
)
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`
// for devices only
Nickname string `json:"nickname",validate:"max=32,alphanum",omitempty`
// for lighthouse only
IsLighthouse bool `json:"isLighthouse",omitempty`
IsRelay bool `json:"isRelay",omitempty`
PublicHostname string `json:"PublicHostname",omitempty`
Port string `json:"port",omitempty`
}
func DeviceCreate(w http.ResponseWriter, req *http.Request) {
@ -67,11 +75,22 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
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
}
_, err3 := c.InsertOne(nil, map[string]interface{}{
"Nickname": nickname,
"DeviceName": deviceName,
"PublicKey": key,
"IP": request.IP,
"IsLighthouse": request.IsLighthouse,
"IsRelay": request.IsRelay,
"PublicHostname": request.PublicHostname,
"Port": request.Port,
})
if err3 != nil {
@ -88,9 +107,24 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
http.StatusInternalServerError, "DC006")
return
}
lightHousesList := []utils.ConstellationDevice{}
if request.IsLighthouse {
lightHousesList, err = GetAllLightHouses()
}
// read configYml from config/nebula.yml
configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml", capki, cert, key)
configYml, err := getYAMLClientConfig(deviceName, utils.CONFIGFOLDER + "nebula.yml", capki, cert, key, utils.ConstellationDevice{
Nickname: nickname,
DeviceName: deviceName,
PublicKey: key,
IP: request.IP,
IsLighthouse: request.IsLighthouse,
IsRelay: request.IsRelay,
PublicHostname: request.PublicHostname,
Port: request.Port,
})
if err != nil {
utils.Error("DeviceCreation: Error while reading config", err)
utils.HTTPError(w, "Device Creation Error: " + err.Error(),
@ -108,6 +142,11 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
"IP": request.IP,
"Config": configYml,
"CA": capki,
"IsLighthouse": request.IsLighthouse,
"IsRelay": request.IsRelay,
"PublicHostname": request.PublicHostname,
"Port": request.Port,
"LighthousesList": lightHousesList,
},
})
} else if err2 == nil {

View file

@ -29,7 +29,7 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
return
}
var devices []utils.Device
var devices []utils.ConstellationDevice
// Check if user is an admin
if isAdmin {
@ -47,11 +47,6 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
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")
@ -68,11 +63,6 @@ func DeviceList(w http.ResponseWriter, req *http.Request) {
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

View file

@ -55,6 +55,26 @@ func API_Restart(w http.ResponseWriter, req *http.Request) {
}
}
func API_Reset(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "GET") {
ResetNebula()
utils.Log("Constellation: nebula reset")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func API_GetLogs(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return

View file

@ -0,0 +1,47 @@
package constellation
import (
"net/http"
"encoding/json"
"io/ioutil"
"github.com/azukaar/cosmos-server/src/utils"
)
func API_ConnectToExisting(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "POST") {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
utils.Error("API_Restart: Invalid User Request", err)
utils.HTTPError(w, "API_Restart Error",
http.StatusInternalServerError, "AR001")
return
}
config := utils.ReadConfigFromFile()
config.ConstellationConfig.Enabled = true
config.ConstellationConfig.SlaveMode = true
config.ConstellationConfig.DNS = false
// ConstellationHostname =
// output utils.CONFIGFOLDER + "nebula.yml"
err = ioutil.WriteFile(utils.CONFIGFOLDER + "nebula.yml", body, 0644)
utils.SetBaseMainConfig(config)
RestartNebula()
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
} else {
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -6,32 +6,36 @@ import (
)
func Init() {
var err error
// if Constellation is enabled
if utils.GetMainConfig().ConstellationConfig.Enabled {
InitConfig()
utils.Log("Initializing Constellation module...")
if !utils.GetMainConfig().ConstellationConfig.SlaveMode {
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 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().ConstellationConfig.ConstellationHostname)
}
// 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.1/24", "", true)
}
// 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.1/24", "", true)
}
// export nebula.yml
utils.Log("Constellation: exporting nebula.yml...")
err := ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml")
// export nebula.yml
utils.Log("Constellation: exporting nebula.yml...")
err := ExportConfigToYAML(utils.GetMainConfig().ConstellationConfig, utils.CONFIGFOLDER + "nebula.yml")
if err != nil {
utils.Error("Constellation: error while exporting nebula.yml", err)
if err != nil {
utils.Error("Constellation: error while exporting nebula.yml", err)
}
}
// start nebula

View file

@ -80,18 +80,92 @@ func RestartNebula() {
Init()
}
func ResetNebula() error {
stop()
utils.Log("Resetting nebula...")
os.RemoveAll(utils.CONFIGFOLDER + "nebula.yml")
os.RemoveAll(utils.CONFIGFOLDER + "ca.crt")
os.RemoveAll(utils.CONFIGFOLDER + "ca.key")
os.RemoveAll(utils.CONFIGFOLDER + "cosmos.crt")
os.RemoveAll(utils.CONFIGFOLDER + "cosmos.key")
// remove everything in db
c, err := utils.GetCollection(utils.GetRootAppId(), "devices")
if err != nil {
return err
}
_, err = c.DeleteMany(nil, map[string]interface{}{})
if err != nil {
return err
}
Init()
return nil
}
func GetAllLightHouses() ([]utils.ConstellationDevice, error) {
c, err := utils.GetCollection(utils.GetRootAppId(), "devices")
if err != nil {
return []utils.ConstellationDevice{}, err
}
var devices []utils.ConstellationDevice
cursor, err := c.Find(nil, map[string]interface{}{
"IsLighthouse": true,
})
cursor.All(nil, &devices)
if err != nil {
return []utils.ConstellationDevice{}, err
}
return devices, nil
}
func cleanIp(ip string) string {
return strings.Split(ip, "/")[0]
}
func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath string) error {
// Combine defaultConfig and overwriteConfig
finalConfig := NebulaDefaultConfig
finalConfig.StaticHostMap = map[string][]string{
"192.168.201.1": []string{
utils.GetMainConfig().HTTPConfig.Hostname + ":4242",
utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
},
}
// for each lighthouse
lh, err := GetAllLightHouses()
if err != nil {
return err
}
for _, l := range lh {
finalConfig.StaticHostMap[cleanIp(l.IP)] = []string{
l.PublicHostname + ":" + l.Port,
}
}
// add other lighthouses
finalConfig.Lighthouse.Hosts = []string{}
for _, l := range lh {
finalConfig.Lighthouse.Hosts = append(finalConfig.Lighthouse.Hosts, cleanIp(l.IP))
}
finalConfig.Relay.AMRelay = overwriteConfig.NebulaConfig.Relay.AMRelay
finalConfig.Relay.Relays = []string{}
for _, l := range lh {
if l.IsRelay && l.IsLighthouse {
finalConfig.Relay.Relays = append(finalConfig.Relay.Relays, cleanIp(l.IP))
}
}
// Marshal the combined config to YAML
yamlData, err := yaml.Marshal(finalConfig)
if err != nil {
@ -118,7 +192,7 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
return nil
}
func getYAMLClientConfig(name, configPath, capki, cert, key string) (string, error) {
func getYAMLClientConfig(name, configPath, capki, cert, key string, device utils.ConstellationDevice) (string, error) {
utils.Log("Exporting YAML config for " + name + " with file " + configPath)
// Read the YAML config file
@ -134,21 +208,38 @@ func getYAMLClientConfig(name, configPath, capki, cert, key string) (string, err
return "", err
}
lh, err := GetAllLightHouses()
if err != nil {
return "", err
}
if staticHostMap, ok := configMap["static_host_map"].(map[interface{}]interface{}); ok {
staticHostMap["192.168.201.1"] = []string{
utils.GetMainConfig().HTTPConfig.Hostname + ":4242",
utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
}
for _, l := range lh {
staticHostMap[cleanIp(l.IP)] = []string{
l.PublicHostname + ":" + l.Port,
}
}
} else {
return "", errors.New("static_host_map not found in nebula.yml")
}
// set lightHouse to false
// set lightHouse
if lighthouseMap, ok := configMap["lighthouse"].(map[interface{}]interface{}); ok {
lighthouseMap["am_lighthouse"] = false
lighthouseMap["am_lighthouse"] = device.IsLighthouse
lighthouseMap["hosts"] = []string{
"192.168.201.1",
}
for _, l := range lh {
if cleanIp(l.IP) != cleanIp(device.IP) {
lighthouseMap["hosts"] = append(lighthouseMap["hosts"].([]string), cleanIp(l.IP))
}
}
} else {
return "", errors.New("lighthouse not found in nebula.yml")
}
@ -162,13 +253,34 @@ func getYAMLClientConfig(name, configPath, capki, cert, key string) (string, err
}
if relayMap, ok := configMap["relay"].(map[interface{}]interface{}); ok {
relayMap["am_relay"] = false
relayMap["relays"] = []string{"192.168.201.1"}
relayMap["am_relay"] = device.IsRelay && device.IsLighthouse
relayMap["relays"] = []string{}
if utils.GetMainConfig().ConstellationConfig.NebulaConfig.Relay.AMRelay {
relayMap["relays"] = append(relayMap["relays"].([]string), "192.168.201.1")
}
for _, l := range lh {
if l.IsRelay && l.IsLighthouse && cleanIp(l.IP) != cleanIp(device.IP) {
relayMap["relays"] = append(relayMap["relays"].([]string), cleanIp(l.IP))
}
}
} else {
return "", errors.New("relay not found in nebula.yml")
}
if listen, ok := configMap["listen"].(map[interface{}]interface{}); ok {
if device.Port != "" {
listen["port"] = device.Port
} else {
listen["port"] = "4242"
}
} else {
return "", errors.New("listen not found in nebula.yml")
}
configMap["deviceName"] = name
configMap["local_dns_overwrite"] = "192.168.201.1"
configMap["public_hostname"] = device.PublicHostname
// export configMap as YML
yamlData, err = yaml.Marshal(configMap)

View file

@ -334,6 +334,8 @@ func InitServer() *mux.Router {
srapi.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices)
srapi.HandleFunc("/api/constellation/restart", constellation.API_Restart)
srapi.HandleFunc("/api/constellation/reset", constellation.API_Reset)
srapi.HandleFunc("/api/constellation/connect", constellation.API_ConnectToExisting)
srapi.HandleFunc("/api/constellation/config", constellation.API_GetConfig)
srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)

View file

@ -46,7 +46,7 @@ func joinURLPath(a, b *url.URL) (path, rawpath string) {
// NewProxy takes target host and creates a reverse proxy
func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardHeader bool, DisableHeaderHardening bool, CORSOrigin string) (*httputil.ReverseProxy, error) {
func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardHeader bool, DisableHeaderHardening bool, CORSOrigin string, route utils.ProxyRouteConfig) (*httputil.ReverseProxy, error) {
url, err := url.Parse(targetHost)
if err != nil {
return nil, err
@ -76,15 +76,28 @@ func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardH
req.Header.Set("X-Forwarded-Ssl", "on")
}
if CORSOrigin != "" {
req.Header.Set("X-Forwarded-Host", url.Host)
req.Header.Del("X-Origin-Host")
req.Header.Del("X-Forwarded-Host")
req.Header.Del("X-Forwarded-For")
req.Header.Del("X-Real-Ip")
req.Header.Del("Host")
hostname := utils.GetMainConfig().HTTPConfig.Hostname
if route.Host != "" && route.UseHost {
hostname = route.Host
}
if route.UsePathPrefix {
hostname = hostname + route.PathPrefix
}
if VerboseForwardHeader {
req.Header.Set("X-Origin-Host", url.Host)
req.Header.Set("Host", url.Host)
req.Header.Set("X-Origin-Host", hostname)
req.Header.Set("Host", hostname)
req.Header.Set("X-Forwarded-Host", hostname)
req.Header.Set("X-Forwarded-For", utils.GetClientIP(req))
req.Header.Set("X-Real-IP", utils.GetClientIP(req))
} else {
req.Host = url.Host
}
}
@ -100,8 +113,6 @@ func NewProxy(targetHost string, AcceptInsecureHTTPSTarget bool, VerboseForwardH
if CORSOrigin != "" {
resp.Header.Del("Access-Control-Allow-Origin")
resp.Header.Del("Access-Control-Allow-Methods")
resp.Header.Del("Access-Control-Allow-Headers")
resp.Header.Del("Access-Control-Allow-Credentials")
}
@ -126,7 +137,7 @@ func RouteTo(route utils.ProxyRouteConfig) http.Handler {
routeType := route.Mode
if(routeType == "SERVAPP" || routeType == "PROXY") {
proxy, err := NewProxy(destination, route.AcceptInsecureHTTPSTarget, route.VerboseForwardHeader, route.DisableHeaderHardening, route.CORSOrigin)
proxy, err := NewProxy(destination, route.AcceptInsecureHTTPSTarget, route.VerboseForwardHeader, route.DisableHeaderHardening, route.CORSOrigin, route)
if err != nil {
utils.Error("Create Route", err)
}

View file

@ -30,12 +30,12 @@ func tokenMiddleware(enabled bool, adminOnly bool) func(next http.Handler) http.
r.Header.Set("x-cosmos-mfa", strconv.Itoa((int)(u.MFAState)))
ogcookies := r.Header.Get("Cookie")
cookieRemoveRegex := regexp.MustCompile(`jwttoken=[^;]*;`)
cookieRemoveRegex := regexp.MustCompile(`\s?jwttoken=[^;]*;?\s?`)
cookies := cookieRemoveRegex.ReplaceAllString(ogcookies, "")
r.Header.Set("Cookie", cookies)
// Replace the token with a application speicfic one
r.Header.Set("x-cosmos-token", "1234567890")
//r.Header.Set("x-cosmos-token", "1234567890")
if enabled && adminOnly {
if errT := utils.AdminOnlyWithRedirect(w, r); errT != nil {

View file

@ -82,8 +82,6 @@ func CORSHeader(origin string) func(next http.Handler) http.Handler {
if origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}

View file

@ -211,6 +211,7 @@ type MarketSource struct {
type ConstellationConfig struct {
Enabled bool
SlaveMode bool
DNS bool
DNSPort string
DNSFallback string
@ -218,6 +219,18 @@ type ConstellationConfig struct {
DNSAdditionalBlocklists []string
CustomDNSEntries map[string]string
NebulaConfig NebulaConfig
ConstellationHostname string
}
type ConstellationDevice struct {
Nickname string `json:"nickname"`
DeviceName string `json:"deviceName"`
PublicKey string `json:"publicKey"`
IP string `json:"ip"`
IsLighthouse bool `json:"isLighthouse"`
IsRelay bool `json:"isRelay"`
PublicHostname string `json:"publicHostname"`
Port string `json:"port"`
}
type NebulaFirewallRule struct {

View file

@ -214,6 +214,15 @@ func LoadBaseMainConfig(config Config) {
if MainConfig.DockerConfig.DefaultDataPath == "" {
MainConfig.DockerConfig.DefaultDataPath = "/usr"
}
if MainConfig.ConstellationConfig.ConstellationHostname == "" {
// if hostname is a domain add vpn. suffix otherwise use hostname
if IsDomain(MainConfig.HTTPConfig.Hostname) {
MainConfig.ConstellationConfig.ConstellationHostname = "vpn." + MainConfig.HTTPConfig.Hostname
} else {
MainConfig.ConstellationConfig.ConstellationHostname = MainConfig.HTTPConfig.Hostname
}
}
}
func GetMainConfig() Config {
@ -577,4 +586,12 @@ func GetClientIP(req *http.Request) string {
ip = req.RemoteAddr
}*/
return req.RemoteAddr
}
func IsDomain(domain string) bool {
// contains . and at least a letter and no special characters invalid in a domain
if strings.Contains(domain, ".") && strings.ContainsAny(domain, "abcdefghijklmnopqrstuvwxyz") && !strings.ContainsAny(domain, " !@#$%^&*()+=[]{}\\|;:'\",/<>?") {
return true
}
return false
}