[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();
@ -92,45 +155,48 @@ const AddDeviceModal = ({ users, config, isAdmin, refreshConfig, devices }) => {
</p>
<Stack spacing={2} direction={"column"}>
{/* {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={"Cosmos Client (File)"} />
<DownloadFile
filename={isDone.DeviceName + `.constellation`}
content={JSON.stringify(isDone, null, 2)}
label={"Download " + isDone.DeviceName + `.constellation`}
/> */}
<CosmosFormDivider title={"File"} />
{/* </>} */}
<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" />
{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 {
@ -89,8 +108,23 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
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,17 +6,20 @@ import (
)
func Init() {
var err error
// if Constellation is enabled
if utils.GetMainConfig().ConstellationConfig.Enabled {
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) {
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)
generateNebulaCACert("Cosmos - " + utils.GetMainConfig().ConstellationConfig.ConstellationHostname)
}
// check if cosmos.crt exists
@ -33,6 +36,7 @@ func Init() {
if err != nil {
utils.Error("Constellation: error while exporting nebula.yml", err)
}
}
// start nebula
utils.Log("Constellation: starting 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 {
@ -578,3 +587,11 @@ func GetClientIP(req *http.Request) string {
}*/
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
}