[release] v0.19.0-unstable1

This commit is contained in:
Yann Stepienik
2025-10-08 19:52:17 +01:00
parent f181788dfa
commit 5e68484e6a
30 changed files with 821 additions and 211 deletions

View File

@@ -2,6 +2,10 @@
echo " ---- Build Cosmos ----"
# Set target architecture for ARM64
# export GOOS=linux
# export GOARCH=arm64
rm -rf build
cp src/update.go src/launcher/update.go

View File

@@ -1,3 +1,14 @@
## Version 0.19.0
- Constellation allows nodes to see and ping each others
- Constellation now has a firewall!
- Constellation now has exit nodes
- Improve docker image cleanup efficiency
- Improve support for container network modes in import/export
- Fixed the annoying "user unauthenticated" error when opening the homepage after the admin token expired
- Fixed issue with exporting hostname when it would be incompatible to re-importing it
- Updating network mode now also updates the network-mode label
- Default storage path is now /cosmos-storage instead of /usr
## Version 0.18.4
- Fix issue with DB credentials dissapearing
- Remove expired discount

View File

@@ -128,6 +128,15 @@ function ping() {
});
}
function pingDevice() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
"data": 1
})
});
}
export {
list,
addDevice,
@@ -138,4 +147,5 @@ export {
connect,
block,
ping,
pingDevice
};

View File

@@ -18,6 +18,15 @@ function ping() {
}))
}
function pingDevice(deviceId) {
return wrap(fetch(`/cosmos/api/constellation/devices/${deviceId}/ping`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function addDevice(device) {
return wrap(fetch('/cosmos/api/constellation/devices', {
method: 'POST',
@@ -128,4 +137,5 @@ export {
connect,
block,
ping,
pingDevice,
};

View File

@@ -139,7 +139,7 @@ const ConfigManagement = () => {
SkipPruneNetwork: config.DockerConfig.SkipPruneNetwork,
SkipPruneImages: config.DockerConfig.SkipPruneImages,
DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr",
DefaultDataPath: config.DockerConfig.DefaultDataPath || "/cosmos-storage",
Background: config && config.HomepageConfig && config.HomepageConfig.Background,
Expanded: config && config.HomepageConfig && config.HomepageConfig.Expanded,
@@ -817,7 +817,7 @@ const ConfigManagement = () => {
label={t('mgmt.config.docker.defaultDatapathInput.defaultDatapathLabel')}
name="DefaultDataPath"
formik={formik}
placeholder={'/usr'}
placeholder={'/cosmos-storage'}
/>
</Stack>
</Stack>

View File

@@ -34,7 +34,7 @@ import defaultport from '../../servapps/defaultport.json';
import * as API from '../../../api';
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label = "Container Name", name = "Target"}) {
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label, name = "Target"}) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const [containers, setContainers] = React.useState([]);
@@ -179,7 +179,7 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
return ( <Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor={name + "-autocomplete"}>{t('mgmt.config.containerPicker.containerNameSelection.containerNameLabel')}</InputLabel>
<InputLabel htmlFor={name + "-autocomplete"}>{label || t('mgmt.config.containerPicker.containerNameSelection.containerNameLabel')}</InputLabel>
{!loading && <Autocomplete
id={name + "-autocomplete"}
open={open}

View File

@@ -75,6 +75,7 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
PublicHostname: '',
IsRelay: true,
isLighthouse: false,
invisible: false,
}}
validationSchema={yup.object({
@@ -185,6 +186,12 @@ const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
formik={formik}
/> */}
<CosmosCheckbox
name="invisible"
label="Invisible (Other clients won't be able to discover this device)"
formik={formik}
/>
{formik.values.isLighthouse && <>
<CosmosFormDivider title={t('mgmt.constellation.setuplighthouseTitle')} />

View File

@@ -1,11 +1,11 @@
import React from "react";
import { useEffect, useState } from "react";
import * as API from "../../api";
import * as API from "../../api";
import AddDeviceModal from "./addDevice";
import PrettyTableView from "../../components/tableView/prettyTableView";
import { DeleteButton } from "../../components/delete";
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, SyncOutlined, TabletOutlined } from "@ant-design/icons";
import { Alert, Button, CircularProgress, IconButton, Stack, Tooltip } from "@mui/material";
import { CloudOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, SyncOutlined, TabletOutlined } from "@ant-design/icons";
import { Alert, Button, Chip, CircularProgress, IconButton, Stack, Switch, Tooltip } from "@mui/material";
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
import MainCard from "../../components/MainCard";
import { Formik } from "formik";
@@ -22,23 +22,25 @@ import { autoBatchEnhancer } from "@reduxjs/toolkit";
const getDefaultConstellationHostname = (config) => {
// if domain is set, use it
if(isDomain(config.HTTPConfig.Hostname)) {
if (isDomain(config.HTTPConfig.Hostname)) {
return "vpn." + config.HTTPConfig.Hostname;
} else {
return config.HTTPConfig.Hostname;
}
}
export const ConstellationVPN = ({freeVersion}) => {
export const ConstellationVPN = ({ freeVersion }) => {
const { t } = useTranslation();
const [config, setConfig] = useState(null);
const [users, setUsers] = useState(null);
const [devices, setDevices] = useState(null);
const [resynDevice, setResyncDevice] = useState(null); // [nickname, deviceName]
const {role} = useClientInfos();
const { role } = useClientInfos();
const isAdmin = role === "2";
const [ping, setPing] = useState(0);
const [coStatus, setCoStatus] = React.useState(null);
const [devicePingStatus, setDevicePingStatus] = useState({}); // {deviceName: 'loading' | 'success' | 'error'}
const [firewallLoading, setFirewallLoading] = useState(null); // deviceName being toggled
const refreshStatus = () => {
API.getStatus().then((res) => {
@@ -46,6 +48,69 @@ export const ConstellationVPN = ({freeVersion}) => {
});
}
const pingDevices = async (deviceList) => {
if (!deviceList || deviceList.length === 0) return;
// Initialize all devices as loading
const initialStatus = {};
deviceList.forEach(device => {
initialStatus[device.deviceName] = 'loading';
});
setDevicePingStatus(initialStatus);
// Ping devices 5 at a time
const batchSize = 5;
for (let i = 0; i < deviceList.length; i += batchSize) {
const batch = deviceList.slice(i, i + batchSize);
const pingPromises = batch.map(device =>
API.constellation.pingDevice(device.deviceName)
.then(res => {
setDevicePingStatus(prev => ({
...prev,
[device.deviceName]: res.data.reachable ? 'success' : 'error'
}));
})
.catch(() => {
setDevicePingStatus(prev => ({
...prev,
[device.deviceName]: 'error'
}));
})
);
await Promise.all(pingPromises);
}
};
const isFirewallBlocked = (deviceName) => {
if (!config?.ConstellationConfig?.FirewallBlockedClients) {
return false;
}
return config.ConstellationConfig.FirewallBlockedClients.includes(deviceName);
};
const toggleFirewallBlock = async (deviceName, isBlocked) => {
setFirewallLoading(deviceName);
let newConfig = { ...config };
if (!newConfig.ConstellationConfig.FirewallBlockedClients) {
newConfig.ConstellationConfig.FirewallBlockedClients = [];
}
if (isBlocked) {
// Remove from blocked list
newConfig.ConstellationConfig.FirewallBlockedClients =
newConfig.ConstellationConfig.FirewallBlockedClients.filter(d => d !== deviceName);
} else {
// Add to blocked list
if (!newConfig.ConstellationConfig.FirewallBlockedClients.includes(deviceName)) {
newConfig.ConstellationConfig.FirewallBlockedClients.push(deviceName);
}
}
await API.config.set(newConfig);
setFirewallLoading(null);
};
let constellationEnabled = config && config.ConstellationConfig.Enabled;
const refreshConfig = async () => {
@@ -53,14 +118,17 @@ export const ConstellationVPN = ({freeVersion}) => {
refreshStatus();
let configAsync = await API.config.get();
setConfig(configAsync.data);
setDevices((await API.constellation.list()).data || []);
if(isAdmin)
const deviceList = (await API.constellation.list()).data || [];
setDevices(deviceList);
if (isAdmin)
setUsers((await API.users.list()).data || []);
else
else
setUsers([]);
if(configAsync.data.ConstellationConfig.Enabled) {
if (configAsync.data.ConstellationConfig.Enabled) {
setPing((await API.constellation.ping()).data ? 2 : 1);
// Ping devices after loading
pingDevices(deviceList.filter((d) => !d.blocked));
}
};
@@ -92,177 +160,255 @@ export const ConstellationVPN = ({freeVersion}) => {
() => setResyncDevice(null)
} />
}
<Stack spacing={2} style={{maxWidth: "1000px", margin: freeVersion ? "auto" : 0}}>
<div>
{constellationEnabled && coStatus.ConstellationSlaveIPWarning && <Alert severity="error">
{coStatus.ConstellationSlaveIPWarning}
</Alert>}
<Stack spacing={2} style={{ maxWidth: "1000px", margin: freeVersion ? "auto" : 0 }}>
<div>
{constellationEnabled && coStatus && coStatus.ConstellationSlaveIPWarning && <Alert severity="error">
{coStatus.ConstellationSlaveIPWarning}
</Alert>}
{!freeVersion && <Alert severity="info">
<Trans i18nKey="mgmt.constellation.setupText"
components={[<a href="https://cosmos-cloud.io/doc/61 Constellation VPN/" target="_blank"></a>, <a href="https://cosmos-cloud.io/clients" target="_blank"></a>]}
/>
</Alert>}
<MainCard title={t('mgmt.constellation.setupTitle')} content={config.constellationIP}>
<Stack spacing={2}>
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
<Alert severity="info">
{t('mgmt.constellation.externalTextSlaveNoAdmin')}
</Alert>
</>}
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
<Alert severity="info">
{t('mgmt.constellation.externalText')}
</Alert>
</>}
{!constellationEnabled && !isAdmin && <>
<Alert severity="info">
{t('mgmt.constellation.setupTextNoAdmin')}
</Alert>
</>}
{(isAdmin || constellationEnabled) && <Formik
enableReinitialize
initialValues={{
Enabled: config.ConstellationConfig.Enabled,
PrivateNode: config.ConstellationConfig.PrivateNode,
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
SyncNodes: !config.ConstellationConfig.DoNotSyncNodes,
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
getDefaultConstellationHostname(config)
}}
onSubmit={(values) => {
let newConfig = { ...config };
newConfig.ConstellationConfig.Enabled = values.Enabled;
newConfig.ConstellationConfig.PrivateNode = values.PrivateNode;
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
newConfig.ConstellationConfig.DoNotSyncNodes = !values.SyncNodes;
setTimeout(() => {
refreshConfig();
}, 1500);
return API.config.set(newConfig);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
{isAdmin && constellationEnabled && <Stack spacing={2} direction="row">
<Button
disableElevation
variant="outlined"
color="primary"
onClick={async () => {
await API.constellation.restart();
}}
>
{t('mgmt.constellation.restartButton')}
</Button>
<ApiModal callback={API.constellation.getLogs} label={t('mgmt.constellation.showLogsButton')} />
<ApiModal callback={API.constellation.getConfig} label={t('mgmt.constellation.showConfigButton')} />
<ConfirmModal
variant="outlined"
color="warning"
label={t('mgmt.constellation.resetLabel')}
content={t('mgmt.constellation.resetText')}
callback={async () => {
await API.constellation.reset();
refreshConfig();
}}
/>
</Stack>}
{constellationEnabled && <div>
{t('mgmt.constellation.constStatus')}: {[
<CircularProgress color="inherit" size={20} />,
<span style={{color: "red"}}>{t('mgmt.constellation.constStatusDown')}</span>,
<span style={{color: "green"}}>{t('mgmt.constellation.constStatusConnected')}</span>,
][ping]}
{!freeVersion && <Alert severity="info">
<Trans i18nKey="mgmt.constellation.setupText"
components={[<a href="https://cosmos-cloud.io/doc/61 Constellation VPN/" target="_blank"></a>, <a href="https://cosmos-cloud.io/clients" target="_blank"></a>]}
/>
</Alert>}
<MainCard title={t('mgmt.constellation.setupTitle')} content={config.constellationIP}>
<Stack spacing={2}>
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
<Alert severity="info">
{t('mgmt.constellation.externalTextSlaveNoAdmin')}
</Alert>
</>}
{constellationEnabled && config.ConstellationConfig.SlaveMode && isAdmin && <>
<Alert severity="info">
{t('mgmt.constellation.externalText')}
</Alert>
</>}
{!constellationEnabled && !isAdmin && <>
<Alert severity="info">
{t('mgmt.constellation.setupTextNoAdmin')}
</Alert>
</>}
{(isAdmin || constellationEnabled) && <Formik
enableReinitialize
initialValues={{
Enabled: config.ConstellationConfig.Enabled,
PrivateNode: config.ConstellationConfig.PrivateNode,
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
SyncNodes: !config.ConstellationConfig.DoNotSyncNodes,
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
getDefaultConstellationHostname(config)
}}
onSubmit={(values) => {
let newConfig = { ...config };
newConfig.ConstellationConfig.Enabled = values.Enabled;
newConfig.ConstellationConfig.PrivateNode = values.PrivateNode;
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
newConfig.ConstellationConfig.DoNotSyncNodes = !values.SyncNodes;
setTimeout(() => {
refreshConfig();
}, 1500);
return API.config.set(newConfig);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
{isAdmin && constellationEnabled && <Stack spacing={2} direction="row">
<Button
disableElevation
variant="outlined"
color="primary"
onClick={async () => {
await API.constellation.restart();
}}
>
{t('mgmt.constellation.restartButton')}
</Button>
<ApiModal callback={API.constellation.getLogs} label={t('mgmt.constellation.showLogsButton')} />
<ApiModal callback={API.constellation.getConfig} label={t('mgmt.constellation.showConfigButton')} />
<ConfirmModal
variant="outlined"
color="warning"
label={t('mgmt.constellation.resetLabel')}
content={t('mgmt.constellation.resetText')}
callback={async () => {
await API.constellation.reset();
refreshConfig();
}}
/>
</Stack>}
<IconButton onClick={async () => {
setPing(0);
setPing((await API.constellation.ping()).data ? 2 : 1);
}}>
<SyncOutlined />
</IconButton>
</div>}
{constellationEnabled && <div>
{t('mgmt.constellation.constStatus')}: {[
<CircularProgress color="inherit" size={20} />,
<span style={{ color: "red" }}>{t('mgmt.constellation.constStatusDown')}</span>,
<span style={{ color: "green" }}>{t('mgmt.constellation.constStatusConnected')}</span>,
][ping]}
{!freeVersion && <>
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="Enabled" label={t('mgmt.constellation.setup.enabledCheckbox')} />
{constellationEnabled && !config.ConstellationConfig.SlaveMode && <>
{formik.values.Enabled && <>
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="IsRelay" label={t('mgmt.constellation.setup.relayRequests.label')} />
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="PrivateNode" label={t('mgmt.constellation.setup.privNode.label')} />
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="SyncNodes" label={t('mgmt.constellation.setup.dataSync.label')} />
{!formik.values.PrivateNode && <>
<Alert severity="info"><Trans i18nKey="mgmt.constellation.setup.hostnameInfo" /></Alert>
<CosmosInputText disabled={!isAdmin} formik={formik} name="ConstellationHostname" label={'Constellation '+t('global.hostname')} />
<IconButton onClick={async () => {
setPing(0);
setPing((await API.constellation.ping()).data ? 2 : 1);
}}>
<SyncOutlined />
</IconButton>
</div>}
{!freeVersion && <>
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="Enabled" label={t('mgmt.constellation.setup.enabledCheckbox')} />
{constellationEnabled && !config.ConstellationConfig.SlaveMode && <>
{formik.values.Enabled && <>
<CosmosCheckbox disabled={!isAdmin} formik={formik} name="SyncNodes" label={t('mgmt.constellation.setup.dataSync.label')} />
{devices.length > 0 && <Alert severity="warning">{t('mgmt.constellation.setup.deviceConnectedWarn')}</Alert>}
<CosmosCheckbox disabled={!isAdmin || devices.length > 0} formik={formik} name="IsRelay" label={t('mgmt.constellation.setup.relayRequests.label')} />
<CosmosCheckbox disabled={!isAdmin || devices.length > 0} formik={formik} name="PrivateNode" label={t('mgmt.constellation.setup.privNode.label')} />
{!formik.values.PrivateNode && <>
<Alert severity="info"><Trans i18nKey="mgmt.constellation.setup.hostnameInfo" /></Alert>
<CosmosInputText disabled={!isAdmin || devices.length > 0} formik={formik} name="ConstellationHostname" label={'Constellation ' + t('global.hostname')} />
</>}
</>}
</>}
{isAdmin && <><LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{t('global.saveAction')}
</LoadingButton>
</>}
</>}
</>}
</>}
{isAdmin && <><UploadButtons
accept=".yml,.yaml"
label={config.ConstellationConfig.SlaveMode ?
t('mgmt.constellation.setup.externalConfig.slaveMode.label')
: t('mgmt.constellation.setup.externalConfig.label')}
variant="outlined"
fullWidth
OnChange={async (e) => {
let file = e.target.files[0];
await API.constellation.connect(file);
setTimeout(() => {
refreshConfig();
}, 1000);
}}
/></>}
</Stack>
</form>
)}
</Formik>}
</Stack>
</MainCard>
</div>
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
<CosmosFormDivider title={"Devices"} />
<PrettyTableView
data={devices.filter((d) => !d.blocked)}
getKey={(r) => r.deviceName}
buttons={[
<AddDeviceModal users={users} config={config} refreshConfig={refreshConfig} devices={devices} />,
<Button
disableElevation
variant="outlined"
color="primary"
onClick={async () => {
pingDevices(devices.filter((d) => !d.blocked));
}}
>
{t('mgmt.constellation.setup.repingAll')}
</Button>
]}
columns={[
{
title: '',
field: getIcon,
},
{
title: t('mgmt.constellation.setup.deviceName.label'),
field: (r) => {
{isAdmin && <><LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
{t('global.saveAction')}
</LoadingButton>
</>}
</>}
{isAdmin && <><UploadButtons
accept=".yml,.yaml"
label={config.ConstellationConfig.SlaveMode ?
t('mgmt.constellation.setup.externalConfig.slaveMode.label')
: t('mgmt.constellation.setup.externalConfig.label')}
variant="outlined"
fullWidth
OnChange={async (e) => {
let file = e.target.files[0];
await API.constellation.connect(file);
setTimeout(() => {
refreshConfig();
}, 1000);
}}
/></>}
</Stack>
</form>
)}
</Formik>}
</Stack>
</MainCard>
</div>
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
<CosmosFormDivider title={"Devices"} />
<PrettyTableView
data={devices.filter((d) => !d.blocked)}
getKey={(r) => r.deviceName}
buttons={[
<AddDeviceModal users={users} config={config} refreshConfig={refreshConfig} devices={devices}/>,
]}
columns={[
{
title: '',
field: getIcon,
const status = devicePingStatus[r.deviceName];
let res = "";
if (status === 'loading') {
res = <CircularProgress size={16} />;
} else if (status === 'success') {
res = "🟢";
} else if (status === 'error') {
res = "🔴";
}
return <strong>{res} {r.deviceName}</strong>;
}
},
{
title: t('mgmt.constellation.setup.deviceName.label'),
field: (r) => <strong>{r.deviceName}</strong>,
title: t('mgmt.constellation.setup.owner.label'),
field: (r) => <strong>{r.nickname}</strong>,
},
{
title: t('mgmt.constellation.setup.owner.label'),
field: (r) => <strong>{r.nickname}</strong>,
title: t('mgmt.storage.typeTitle'),
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
},
{
title: t('mgmt.storage.typeTitle'),
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
title: t('mgmt.constellation.setup.ipTitle'),
screenMin: 'md',
field: (r) => r.ip,
},
{
title: t('mgmt.constellation.setup.ipTitle'),
screenMin: 'md',
field: (r) => r.ip,
title: t('mgmt.constellation.setup.firewallStatus'),
field: (r) => {
const blocked = isFirewallBlocked(r.deviceName);
const isLoading = firewallLoading === r.deviceName;
if (isLoading) {
return <Chip
label={
<div>
<CircularProgress size={16} style={{ verticalAlign: "middle", marginRight: 4 }} />
Updating...
</div>
}
color="default"
/>;
}
return blocked ? <Chip
label={
<div>
<Switch size="small" style={{ verticalAlign: "middle", marginRight: 4 }} />
Blocked
</div>
}
color="error"
onClick={() => toggleFirewallBlock(r.deviceName, blocked)}
style={{ cursor: 'pointer' }}
/> : <Chip
label={
<div>
<Switch
size="small"
sx={{
marginTop: "-3px",
'& .MuiSwitch-switchBase.Mui-checked': {
color: "white",
},
'& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
backgroundColor: "white",
},
}}
checked
/>
Allowed
</div>
}
color="success"
onClick={() => toggleFirewallBlock(r.deviceName, blocked)}
style={{ cursor: 'pointer' }}
/>;
},
},
{
title: '',
@@ -281,14 +427,14 @@ export const ConstellationVPN = ({freeVersion}) => {
</>
}
}
]}
/>
</>}
</Stack>
]}
/>
</>}
</Stack>
</> : <center>
<CircularProgress color="inherit" size={20} />
</center>}
{freeVersion && config && !constellationEnabled && <VPNSalesPage />}
{freeVersion && config && !constellationEnabled && <VPNSalesPage />}
</>
};

View File

@@ -124,7 +124,7 @@ const convertDockerCompose = (config, serviceName, dockerCompose, setYmlError) =
if(doc.services[key].volumes)
Object.values(doc.services[key].volumes).forEach((volume) => {
if (volume.source && volume.source[0] === '.') {
let defaultPath = (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/usr"
let defaultPath = (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/cosmos-storage"
volume.source = defaultPath + volume.source.replace('.', '');
}
});
@@ -324,7 +324,7 @@ const convertDockerCompose = (config, serviceName, dockerCompose, setYmlError) =
// for each network mode that are container, add a label and remove hostname
Object.keys(doc.services).forEach((key) => {
if (doc.services[key].network_mode && doc.services[key].network_mode.startsWith('container:')) {
if (doc.services[key].network_mode && (doc.services[key].network_mode.startsWith('service:') || doc.services[key].network_mode.startsWith('container:'))) {
doc.services[key].labels = doc.services[key].labels || {};
doc.services[key].labels['cosmos-force-network-mode'] = doc.services[key].network_mode;
@@ -549,7 +549,7 @@ const DockerComposeImport = ({ refresh, dockerComposeInit, installerInit, defaul
Passwords: passwords,
CPU_ARCH: API.CPU_ARCH,
CPU_AVX: API.CPU_AVX,
DefaultDataPath: (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/usr",
DefaultDataPath: (config && config.DockerConfig && config.DockerConfig.DefaultDataPath) || "/cosmos-storage",
});
let jsoned;

View File

@@ -12,6 +12,7 @@ import { NetworksColumns } from '../networks';
import NewNetworkButton from '../createNetwork';
import LinkContainersButton from '../linkContainersButton';
import { useTranslation } from 'react-i18next';
import { CosmosContainerPicker } from '../../config/users/containerPicker';
const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, OnChange, OnConnect, OnDisconnect }) => {
const { t } = useTranslation();
@@ -93,6 +94,11 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
initialValues={{
networkMode: containerInfo.HostConfig.NetworkMode,
ports: getPortBindings(),
Container: (() => {
if(!containerInfo || !containerInfo.HostConfig || !containerInfo.HostConfig.NetworkMode) return "";
if(!containerInfo.HostConfig.NetworkMode.startsWith("container:")) return "";
return containerInfo.HostConfig.NetworkMode.replace("container:", "");
})()
}}
validate={(values) => {
const errors = {};
@@ -142,16 +148,29 @@ const NetworkContainerSetup = ({ config, containerInfo, refresh, newContainer, O
{t('mgmt.servApps.networks.containerotRunningWarning')}
</Alert>
)}
{isForceSecure && (
{/* {isForceSecure && (
<Alert severity="warning" style={{ marginBottom: '0px' }}>
{t('mgmt.servApps.networks.forcedSecurityWarning')}
</Alert>
)}
)} */}
<CosmosInputText
label={t('mgmt.servApps.networks.modeInput.modeLabel')}
name="networkMode"
placeholder={'default'}
formik={formik}
onChange={(e) => {
formik.setFieldValue('Container', '');
}}
/>
<CosmosContainerPicker
formik={formik}
onTargetChange={(_, name) => {
if(!name) return;
formik.setFieldValue('networkMode', `container:${name}`);
}}
name='Container'
label={t('mgmt.servApps.networks.useAsVPN')}
nameOnly
/>
<CosmosFormDivider title={t('mgmt.servApps.networks.exposePortsTitle')} />
<div>

View File

@@ -354,11 +354,12 @@ const ServApps = ({stack}) => {
Ports
</Typography>
<Stack style={noOver} margin={1} direction="row" spacing={1}>
{app.ports.filter(p => p.IP != '::').map((port) => {
{console.log(app.ports)}
{app.ports && (app.ports.filter(p => p && p.IP != '::').map((port) => {
return <Tooltip title={port.PublicPort ? 'Warning, this port is publicly accessible' : ''}>
<Chip style={{ fontSize: '80%' }} label={(port.PublicPort ? (port.PublicPort + ":") : '') + port.PrivatePort} color={port.PublicPort ? 'warning' : 'default'} />
</Tooltip>
})}
}))}
</Stack>
</Stack>
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">

View File

@@ -19,15 +19,26 @@ function useClientInfos() {
// Try to parse the cookie into a JavaScript object
clientInfos = cookies['client-infos'].split(',');
if(clientInfos.length !== 3) {
if(clientInfos.length <= 3) {
window.location.href = '/cosmos-ui/logout';
}
return {
let res = {
nickname: clientInfos[0],
userRole: clientInfos[1],
role: clientInfos[2]
};
if(clientInfos.length > 3) {
let roleUntil = new Date(parseInt(clientInfos[3], 10) * 1000);
let currentDate = new Date();
if(roleUntil < currentDate && res.userRole == "2") {
res.userRole = "1";
}
}
return res;
} catch (error) {
console.error('Error parsing client-infos cookie:', error);
return {

View File

@@ -273,6 +273,9 @@
"mgmt.constellation.setup.relayRequests.label": "Relay requests via this Node",
"mgmt.constellation.setup.unsafeRoutesText": "Coming soon. This feature will allow you to tunnel your traffic through your devices to things outside of your constellation.",
"mgmt.constellation.setup.unsafeRoutesTitle": "Unsafe Routes",
"mgmt.constellation.setup.repingAll": "Ping All",
"mgmt.constellation.setup.deviceConnectedWarn": "The following settings cannot be changed once clients have been created. Reset your network to change them.",
"mgmt.constellation.setup.firewallStatus": "Firewall",
"mgmt.constellation.setupText": "Constellation is a VPN that runs inside your Cosmos network. It automatically connects all your devices together, and allows you to access them from anywhere. Please refer to the <0>documentation</0> for more information. In order to connect, please use the <1>Constellation App</1>",
"mgmt.constellation.setupTitle": "Constellation Setup",
"mgmt.constellation.setuplighthouseTitle": "Lighthouse Setup",
@@ -363,6 +366,7 @@
"mgmt.servApps.networks.removedNetConnectedEitherRecreate": "Either re-create it or",
"mgmt.servApps.networks.removedNetConnectedError": "You are connected to a network that has been removed:",
"mgmt.servApps.networks.updatePortsButton": "Update Ports",
"mgmt.servApps.networks.useAsVPN": "Use this container as VPN or Network controller",
"mgmt.servApps.newChip.newLabel": "New",
"mgmt.servApps.newContainer.chooseUrl": "Choose URL for",
"mgmt.servApps.newContainer.cosmosOutdatedError": "This service requires a newer version of Cosmos. Please update Cosmos to install this service.",

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "cosmos-server",
"version": "0.18.0-unstable8",
"version": "0.19.0-unstable1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cosmos-server",
"version": "0.18.0-unstable8",
"version": "0.19.0-unstable1",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.7.0",

View File

@@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.18.4",
"version": "0.19.0-unstable1",
"description": "",
"main": "test-server.js",
"bugs": {

View File

@@ -396,14 +396,17 @@ func SlaveConfigSync(newConfig string) (bool, error) {
return false, err
}
endpoint := configMap["cstln_config_endpoint"]
rawEndpoint := configMap["cstln_config_endpoint"]
apiKey := configMap["cstln_api_key"]
if endpoint == nil || apiKey == nil {
if rawEndpoint == nil || apiKey == nil {
utils.Error("SlaveConfigSync: Invalid slave config file for resync", nil)
return false, errors.New("Invalid slave config file for resync")
}
endpoint := rawEndpoint.(string)
endpoint += "cosmos/api/constellation/config-sync"
// utils.Log("SlaveConfigSync: Fetching config from " + endpoint.(string))
// fetch the config from the endpoint with Authorization header

View File

@@ -21,6 +21,7 @@ type DeviceCreateRequestJSON struct {
IsRelay bool `json:"isRelay",omitempty`
PublicHostname string `json:"PublicHostname",omitempty`
Port string `json:"port",omitempty`
Invisible bool `json:"invisible",omitempty`
}
func DeviceCreate(w http.ResponseWriter, req *http.Request) {
@@ -114,6 +115,7 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
"Fingerprint": fingerprint,
"APIKey": APIKey,
"Blocked": false,
"Invisible": request.Invisible,
})
if err3 != nil {
@@ -185,6 +187,7 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
"PublicHostname": request.PublicHostname,
"Port": request.Port,
"LighthousesList": lightHousesList,
"Invisible": request.Invisible,
},
})

View File

@@ -0,0 +1,94 @@
package constellation
import (
"net/http"
"encoding/json"
"os/exec"
"runtime"
"github.com/gorilla/mux"
"github.com/azukaar/cosmos-server/src/utils"
)
func DevicePing(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
return
}
if utils.LoggedInOnly(w, req) != nil {
return
}
// Extract device ID from URL parameters
vars := mux.Vars(req)
deviceID := vars["id"]
if deviceID == "" {
utils.HTTPError(w, "Device ID is required", http.StatusBadRequest, "DP001")
return
}
// Connect to the collection
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
defer closeDb()
if errCo != nil {
utils.Error("Database Connect", errCo)
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
return
}
var device utils.ConstellationDevice
// Find the device by DeviceName
err := c.FindOne(nil, map[string]interface{}{
"DeviceName": deviceID,
}).Decode(&device)
if err != nil {
utils.Error("DevicePing: Device not found", err)
utils.HTTPError(w, "Device not found", http.StatusNotFound, "DP002")
return
}
// Check if the device IP exists
if device.IP == "" {
utils.HTTPError(w, "Device has no IP address", http.StatusBadRequest, "DP003")
return
}
// Ping the device IP
pingResult := pingIP(device.IP)
// Respond with the ping result
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{
"deviceName": device.DeviceName,
"ip": device.IP,
"reachable": pingResult,
},
})
}
func pingIP(ip string) bool {
var cmd *exec.Cmd
// remove the CIDR suffix if present
if len(ip) > 3 && ip[len(ip)-3] == '/' {
ip = ip[:len(ip)-3]
}
// Use platform-specific ping command
if runtime.GOOS == "windows" {
cmd = exec.Command("ping", "-n", "1", "-w", "10000", ip)
} else {
cmd = exec.Command("ping", "-c", "1", "-W", "10", ip)
}
output, err := cmd.CombinedOutput()
utils.Debug("Ping - Pinging IP "+ip+" - Reachable: \nOutput:\n"+string(output))
return err == nil
}

View File

@@ -0,0 +1,92 @@
package constellation
import (
"net/http"
"encoding/json"
"strings"
"github.com/azukaar/cosmos-server/src/utils"
)
// PublicDeviceInfo represents the limited device information exposed to the public API
type PublicDeviceInfo struct {
DeviceID string `json:"id"`
DeviceName string `json:"name"`
User string `json:"user"`
IP string `json:"ip"`
}
func DevicePublicList(w http.ResponseWriter, req *http.Request) {
// Check for GET method
if req.Method != "GET" {
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP002")
return
}
// Get authorization header
auth := req.Header.Get("Authorization")
if auth == "" {
http.Error(w, "Unauthorized [1]", http.StatusUnauthorized)
return
}
// Remove "Bearer " from auth header
auth = strings.Replace(auth, "Bearer ", "", 1)
// Connect to the collection
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
defer closeDb()
if errCo != nil {
utils.Error("Database Connect", errCo)
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
return
}
utils.Log("DevicePublicList: Fetching devices with API key")
// Find all non-blocked devices that match the API key
cursor, err := c.Find(nil, map[string]interface{}{
"Blocked": false,
"Invisible": false,
})
defer cursor.Close(nil)
if err != nil {
utils.Error("DevicePublicList: Error fetching devices", err)
utils.HTTPError(w, "Error fetching devices", http.StatusInternalServerError, "DPL001")
return
}
var devices []utils.ConstellationDevice
if err = cursor.All(nil, &devices); err != nil {
utils.Error("DevicePublicList: Error decoding devices", err)
utils.HTTPError(w, "Error decoding devices", http.StatusInternalServerError, "DPL002")
return
}
// Always add the cosmos lighthouse device
cosmosDevice := utils.ConstellationDevice{
DeviceName: "cosmos",
Nickname: "cosmos",
IP: "192.168.201.1",
}
devices = append([]utils.ConstellationDevice{cosmosDevice}, devices...)
// Convert to public device info with limited fields
publicDevices := make([]PublicDeviceInfo, len(devices))
for i, device := range devices {
publicDevices[i] = PublicDeviceInfo{
DeviceID: device.DeviceName,
DeviceName: device.DeviceName,
User: device.Nickname,
IP: cleanIp(device.IP),
}
}
// Respond with the list of public device info
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": publicDevices,
})
}

View File

@@ -40,6 +40,8 @@ var NebulaFailedStarting = false
func startNebulaInBackground() error {
ProcessMux.Lock()
defer ProcessMux.Unlock()
UpdateFirewallBlockedClients()
NebulaFailedStarting = false
if process != nil {
@@ -249,6 +251,7 @@ func ResetNebula() error {
config.ConstellationConfig.Enabled = false
config.ConstellationConfig.SlaveMode = false
config.ConstellationConfig.DNSDisabled = false
config.ConstellationConfig.FirewallBlockedClients = []string{}
utils.SetBaseMainConfig(config)
@@ -356,7 +359,7 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
for _, d := range blockedDevices {
finalConfig.PKI.Blocklist = append(finalConfig.PKI.Blocklist, d.Fingerprint)
}
finalConfig.Lighthouse.AMLighthouse = !overwriteConfig.PrivateNode
finalConfig.Lighthouse.Hosts = []string{}
@@ -402,6 +405,136 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
return nil
}
func UpdateFirewallBlockedClients() error {
nebulaYmlPath := utils.CONFIGFOLDER + "nebula.yml"
// Read the existing nebula.yml file
yamlData, err := ioutil.ReadFile(nebulaYmlPath)
if err != nil {
return fmt.Errorf("failed to read nebula.yml: %w", err)
}
// Unmarshal the YAML data into a map
var configMap map[string]interface{}
err = yaml.Unmarshal(yamlData, &configMap)
if err != nil {
return fmt.Errorf("failed to unmarshal nebula.yml: %w", err)
}
// Get the firewall configuration
firewallMap, ok := configMap["firewall"].(map[interface{}]interface{})
if !ok {
return errors.New("firewall not found in nebula.yml")
}
// Get all devices from the database to map names to IPs
c, closeDb, errCo := utils.GetEmbeddedCollection(utils.GetRootAppId(), "devices")
if errCo != nil {
return errCo
}
defer closeDb()
var devices []utils.ConstellationDevice
cursor, err := c.Find(nil, map[string]interface{}{})
if err != nil {
return err
}
defer cursor.Close(nil)
cursor.All(nil, &devices)
// Always add the cosmos lighthouse device
cosmosDevice := utils.ConstellationDevice{
DeviceName: "cosmos",
Nickname: "cosmos",
IP: "192.168.201.1",
}
devices = append([]utils.ConstellationDevice{cosmosDevice}, devices...)
// Create a map of device names to IPs
deviceIPs := make(map[string]string)
for _, device := range devices {
deviceIPs[device.DeviceName] = cleanIp(device.IP)
}
// Get the blocked clients list from config
blockedClients := utils.GetMainConfig().ConstellationConfig.FirewallBlockedClients
blockedIPs := make(map[string]bool)
for _, clientName := range blockedClients {
if ip, exists := deviceIPs[clientName]; exists && ip != "" {
blockedIPs[ip] = true
utils.Log("Constellation: Blocking device " + clientName + " (" + ip + ") in firewall")
}
}
// Build new firewall rules
newInboundRules := []interface{}{}
newOutboundRules := []interface{}{}
// Always allow ICMP (ping) from any
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
"port": "any",
"proto": "icmp",
"host": "any",
})
// Always allow port 4222 (NATS) from any
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
"port": 4222,
"proto": "any",
"host": "any",
})
// Always allow port 53 (DNS) from any
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
"port": 53,
"proto": "any",
"host": "any",
})
// Always allow lighthouse (cosmos)
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
"port": "any",
"proto": "any",
"host": "cosmos",
})
// Allow outbound to any
newOutboundRules = append(newOutboundRules, map[interface{}]interface{}{
"port": "any",
"proto": "any",
"host": "any",
})
for _, device := range devices {
if device.DeviceName != "" && !blockedIPs[cleanIp(device.IP)] && device.DeviceName != "cosmos" {
// Allow inbound from this device using its hostname
newInboundRules = append(newInboundRules, map[interface{}]interface{}{
"port": "any",
"proto": "any",
"host": device.DeviceName,
})
}
}
firewallMap["inbound"] = newInboundRules
firewallMap["outbound"] = newOutboundRules
// Marshal back to YAML
updatedYaml, err := yaml.Marshal(configMap)
if err != nil {
return fmt.Errorf("failed to marshal updated config: %w", err)
}
// Write back to nebula.yml
err = ioutil.WriteFile(nebulaYmlPath, updatedYaml, 0644)
if err != nil {
return fmt.Errorf("failed to write nebula.yml: %w", err)
}
utils.Log("Updated firewall rules in nebula.yml")
return nil
}
func getYAMLClientConfig(name, configPath, capki, cert, key, APIKey string, device utils.ConstellationDevice, lite bool, getLicence bool) (string, error) {
utils.Log("Exporting YAML config for " + name + " with file " + configPath)
@@ -525,7 +658,7 @@ func getYAMLClientConfig(name, configPath, capki, cert, key, APIKey string, devi
configHost += "/"
}
configEndpoint := configHost + "cosmos/api/constellation/config-sync"
configEndpoint := configHost
configHostname := strings.Split(configHost, "://")[1]
configHostname = strings.Split(configHostname, ":")[0]
@@ -772,6 +905,7 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, s
"sign",
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
"-ca-key", utils.CONFIGFOLDER + "ca.key",
"-subnets", "0.0.0.0/0",
"-name", name,
"-ip", ip,
)
@@ -785,6 +919,7 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, s
"sign",
"-ca-crt", utils.CONFIGFOLDER + "ca.crt",
"-ca-key", utils.CONFIGFOLDER + "ca.key",
"-subnets", "0.0.0.0/0",
"-name", name,
"-ip", ip,
"-in-pub", "./temp.key",

View File

@@ -423,6 +423,7 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
// Create containers
tempServiceList := make(map[string]ContainerCreateRequestContainer)
for serviceName, container := range serviceRequest.Services {
utils.Log(fmt.Sprintf("Checking service %s...", serviceName))
OnLog(fmt.Sprintf("Checking service %s...\n", serviceName))
@@ -491,10 +492,6 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
OpenStdin: container.StdinOpen,
}
if container.StopGracePeriod == 0 {
containerConfig.StopTimeout = nil
}
// check if there's an empty TZ env, if so, replace it with the host's TZ
if containerConfig.Env != nil {
for i, env := range containerConfig.Env {
@@ -699,6 +696,18 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
CapDrop: container.CapDrop,
}
// cosmos-force-network-mode logic
if containerConfig.Labels["cosmos-force-network-mode"] == "" {
if (strings.HasPrefix(string(hostConfig.NetworkMode), "service:") ||
strings.HasPrefix(string(hostConfig.NetworkMode), "container:")) {
containerConfig.Labels["cosmos-force-network-mode"] = string(hostConfig.NetworkMode)
}
} else {
hostConfig.NetworkMode = conttype.NetworkMode(containerConfig.Labels["cosmos-force-network-mode"])
utils.Debug("Forcing network mode to " + string(hostConfig.NetworkMode))
}
if container.Runtime != "" {
hostConfig.Runtime = strings.Join(strings.Fields(container.Runtime), " ")
}
@@ -903,10 +912,16 @@ func CreateService(serviceRequest DockerServiceCreateRequest, OnLog func(string)
// Write a response to the client
utils.Log(fmt.Sprintf("Container %s created", container.Name))
OnLog(fmt.Sprintf("Container %s created", container.Name))
tempServiceList[serviceName] = ContainerCreateRequestContainer{
Name: container.Name,
DependsOn: container.DependsOn,
NetworkMode: string(hostConfig.NetworkMode),
}
}
// re-order containers dpeneding on depends_on
startOrder, mustStart, err := ReOrderServices(serviceRequest.Services)
startOrder, mustStart, err := ReOrderServices(tempServiceList)
if err != nil {
utils.Error("CreateService: Rolling back changes because of -- Container", err)
OnLog(utils.DoErr("Rolling back changes because of -- Container creation error: "+err.Error()))
@@ -1044,9 +1059,23 @@ func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]C
changed := false
for name, service := range serviceMap {
dependencies := service.DependsOn
if dependencies == nil {
dependencies = make(map[string]ContainerCreateRequestContainerDependsOnCont)
}
// if network_mode is container: then we need to add a dependency
if strings.HasPrefix(string(service.NetworkMode), "container:") {
depService := strings.TrimPrefix(string(service.NetworkMode), "container:")
dependencies[depService] = ContainerCreateRequestContainerDependsOnCont{
Condition: "service_started",
}
}
// If there are no dependencies, we can add this service to startOrder
// Check if all dependencies are already in startOrder
allDependenciesStarted := true
for dependency, dependencyDetails := range service.DependsOn {
for dependency, dependencyDetails := range dependencies {
dependencyStarted := false
for _, startedService := range startOrder {
if startedService.Name == dependency {
@@ -1086,6 +1115,13 @@ func ReOrderServices(serviceMap map[string]ContainerCreateRequestContainer) ([]C
for name, _ := range serviceMap {
errorMessage += "Could not start service: " + name + "\n"
errorMessage += "Unsatisfied dependencies:\n"
// if network_mode is container: then we need to add a dependency
if strings.HasPrefix(string(serviceMap[name].NetworkMode), "container:") {
depService := strings.TrimPrefix(string(serviceMap[name].NetworkMode), "container:")
errorMessage += depService + " (network_mode)\n"
}
for dependency, _ := range serviceMap[name].DependsOn {
_, ok := serviceMap[dependency]
if ok {

View File

@@ -125,6 +125,11 @@ func UpdateContainerRoute(w http.ResponseWriter, req *http.Request) {
form.NetworkMode != "default" {
container.Config.MacAddress = ""
}
// update cosmos-force-network-mode label
if container.Config.Labels == nil {
container.Config.Labels = make(map[string]string)
}
container.Config.Labels["cosmos-force-network-mode"] = form.NetworkMode
}
_, err = EditContainer(container.ID, container, false)

View File

@@ -130,15 +130,13 @@ func EditContainer(oldContainerID string, newConfig types.ContainerJSON, noLock
}
}
if(newConfig.HostConfig.NetworkMode != "bridge" &&
newConfig.HostConfig.NetworkMode != "default" &&
newConfig.HostConfig.NetworkMode != "host" &&
newConfig.HostConfig.NetworkMode != "none") {
if(!HasLabel(newConfig, "cosmos-force-network-mode")) {
if !HasLabel(newConfig, "cosmos-force-network-mode") {
if (strings.HasPrefix(string(newConfig.HostConfig.NetworkMode), "service:") ||
strings.HasPrefix(string(newConfig.HostConfig.NetworkMode), "container:")) {
AddLabels(newConfig, map[string]string{"cosmos-force-network-mode": string(newConfig.HostConfig.NetworkMode)})
} else {
newConfig.HostConfig.NetworkMode = container.NetworkMode(GetLabel(newConfig, "cosmos-force-network-mode"))
}
}
} else {
newConfig.HostConfig.NetworkMode = container.NetworkMode(GetLabel(newConfig, "cosmos-force-network-mode"))
}
newName := newConfig.Name

View File

@@ -41,7 +41,12 @@ func ExportContainer(containerID string) (ContainerCreateRequestContainer, error
User: detailedInfo.Config.User,
Tty: detailedInfo.Config.Tty,
StdinOpen: detailedInfo.Config.OpenStdin,
Hostname: detailedInfo.Config.Hostname,
Hostname: func () string {
if string(detailedInfo.HostConfig.NetworkMode) == "bridge" || string(detailedInfo.HostConfig.NetworkMode) == "default" {
return detailedInfo.Config.Hostname
}
return ""
}(),
Domainname: detailedInfo.Config.Domainname,
MacAddress: detailedInfo.NetworkSettings.MacAddress,
NetworkMode: string(detailedInfo.HostConfig.NetworkMode),

View File

@@ -494,6 +494,7 @@ func NetworkCleanUp() {
if(!config.DockerConfig.SkipPruneImages) {
pruneFilters := filters.NewArgs()
pruneFilters.Add("dangling", "false")
report, err := DockerClient.ImagesPrune(DockerContext, pruneFilters)
if err != nil {
utils.Error("[DOCKER] Error pruning images", err)

View File

@@ -542,6 +542,8 @@ func InitServer() *mux.Router {
srapiAdmin.HandleFunc("/api/get-backup", configapi.BackupFileApiGet)
srapiAdmin.HandleFunc("/api/constellation/devices", constellation.ConstellationAPIDevices)
srapiAdmin.HandleFunc("/api/constellation/public-devices", constellation.DevicePublicList)
srapiAdmin.HandleFunc("/api/constellation/devices/{id}/ping", constellation.DevicePing)
srapiAdmin.HandleFunc("/api/constellation/restart", constellation.API_Restart)
srapiAdmin.HandleFunc("/api/constellation/reset", constellation.API_Reset)
srapiAdmin.HandleFunc("/api/constellation/connect", constellation.API_ConnectToExisting)

View File

@@ -46,6 +46,13 @@ func Init() {
Name: "cosmos-cloud",
}
if config.Mpdu_ != "" && config.Mpdn_ != "" {
defaultMarket = utils.MarketSource{
Url: config.Mpdu_,
Name: config.Mpdn_,
}
}
sources = append([]utils.MarketSource{defaultMarket}, sources...)
for _, marketDef := range sources {

View File

@@ -281,9 +281,11 @@ func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mf
claims["mfaDone"] = mfaDone
claims["forDomain"] = reqHostNoPort
sudoUntil := time.Now().Add(time.Hour * 2).Unix()
// if role is ADMIN, add a timeout
if tokenRole == utils.ADMIN {
claims["sudo-until"] = time.Now().Add(time.Hour * 2).Unix()
claims["sudo-until"] = sudoUntil
}
key, err5 := jwt.ParseEdPrivateKeyFromPEM([]byte(utils.GetPrivateAuthKey()))
@@ -313,7 +315,7 @@ func SendUserToken(w http.ResponseWriter, req *http.Request, user utils.User, mf
clientCookie := http.Cookie{
Name: "client-infos",
Value: user.Nickname + "," + strconv.Itoa(int(user.Role)) + "," + strconv.Itoa(int(tokenRole)),
Value: user.Nickname + "," + strconv.Itoa(int(user.Role)) + "," + strconv.Itoa(int(tokenRole)) + "," + strconv.Itoa(int(sudoUntil - 1)),
Expires: expiration,
Path: "/",
Secure: shouldCookieBeSecured(req.RemoteAddr),

View File

@@ -111,6 +111,8 @@ type Config struct {
RemoteStorage RemoteStorageConfig
DisableOpenIDDirect bool
Backup BackupConfig
Mpdu_ string
Mpdn_ string
}
@@ -297,6 +299,7 @@ type ConstellationConfig struct {
NebulaConfig NebulaConfig
ConstellationHostname string
Tunnels []ProxyRouteConfig
FirewallBlockedClients []string `json:"FirewallBlockedClients" bson:"FirewallBlockedClients"`
}
type ConstellationDNSEntry struct {
@@ -317,6 +320,7 @@ type ConstellationDevice struct {
Blocked bool `json:"blocked" bson:"Blocked"`
Fingerprint string `json:"fingerprint" bson:"Fingerprint"`
APIKey string `json:"-" bson:"APIKey"`
Invisible bool `json:"invisible" bson:"Invisible"`
}
type NebulaFirewallRule struct {

View File

@@ -89,7 +89,7 @@ var DefaultConfig = Config{
},
},
DockerConfig: DockerConfig{
DefaultDataPath: "/usr",
DefaultDataPath: "/cosmos-storage",
},
MarketConfig: MarketConfig{
Sources: []MarketSource{