瀏覽代碼

[release] v0.10.0-unstable16

Yann Stepienik 1 年之前
父節點
當前提交
9b033696e3

+ 13 - 0
client/src/api/constellation.tsx

@@ -66,6 +66,18 @@ function connect(file) {
   }))
 }
 
+function block(nickname, devicename, block) {
+  return wrap(fetch(`/cosmos/api/constellation/block`, {
+    method: 'POST',
+    headers: {
+        'Content-Type': 'application/json'
+    },
+    body: JSON.stringify({
+      nickname, devicename, block
+    }),
+  }))
+}
+
 export {
   list,
   addDevice,
@@ -74,4 +86,5 @@ export {
   getLogs,
   reset,
   connect,
+  block,
 };

+ 14 - 7
client/src/pages/config/users/formShortcuts.jsx

@@ -27,26 +27,33 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
 
 import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
 
-export const CosmosInputText = ({ name, style, multiline, type, placeholder, onChange, label, formik }) => {
+export const CosmosInputText = ({ name, style, value, errors, multiline, type, placeholder, onChange, label, formik }) => {
   return <Grid item xs={12}>
     <Stack spacing={1} style={style}>
-      <InputLabel htmlFor={name}>{label}</InputLabel>
+      {label && <InputLabel htmlFor={name}>{label}</InputLabel>}
       <OutlinedInput
         id={name}
         type={type ? type : 'text'}
-        value={formik.values[name]}
+        value={value || (formik && formik.values[name])}
         name={name}
         multiline={multiline}
-        onBlur={formik.handleBlur}
+        onBlur={(...ar) => {
+          return formik && formik.handleBlur(...ar);
+        }}
         onChange={(...ar) => {
           onChange && onChange(...ar);
-          return formik.handleChange(...ar);
+          return formik && formik.handleChange(...ar);
         }}
         placeholder={placeholder}
         fullWidth
-        error={Boolean(formik.touched[name] && formik.errors[name])}
+        error={Boolean(formik && formik.touched[name] && formik.errors[name])}
       />
-      {formik.touched[name] && formik.errors[name] && (
+      {formik && formik.touched[name] && formik.errors[name] && (
+        <FormHelperText error id="standard-weight-helper-text-name-login">
+          {formik.errors[name]}
+        </FormHelperText>
+      )}
+      {errors && (
         <FormHelperText error id="standard-weight-helper-text-name-login">
           {formik.errors[name]}
         </FormHelperText>

+ 174 - 0
client/src/pages/constellation/dns.jsx

@@ -0,0 +1,174 @@
+import React from "react";
+import { useEffect, useState } from "react";
+import * as API  from "../../api";
+import AddDeviceModal from "./addDevice";
+import PrettyTableView from "../../components/tableView/prettyTableView";
+import { DeleteButton } from "../../components/delete";
+import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
+import IsLoggedIn from "../../isLoggedIn";
+import { Alert, Button, CircularProgress, InputLabel, 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";
+
+export const ConstellationDNS = () => {
+  const [isAdmin, setIsAdmin] = useState(false);
+  const [config, setConfig] = useState(null);
+
+  const refreshConfig = async () => {
+    let configAsync = await API.config.get();
+    setConfig(configAsync.data);
+    setIsAdmin(configAsync.isAdmin);
+  };
+
+  useEffect(() => {
+    refreshConfig();
+  }, []);
+
+  return <>
+    <IsLoggedIn />
+    {(config) ? <>
+      <Stack spacing={2} style={{maxWidth: "1000px"}}>
+      <div>
+        <MainCard title={"Constellation Internal DNS"} content={config.constellationIP}>
+          <Stack spacing={2}>
+
+          <Formik
+            initialValues={{
+              Enabled: config.ConstellationConfig.DNS,
+              Port: config.ConstellationConfig.DNSPort,
+              Fallback: config.ConstellationConfig.DNSFallback,
+              DNSBlockBlacklist: config.ConstellationConfig.DNSBlockBlacklist,
+              DNSAdditionalBlocklists: config.ConstellationConfig.DNSAdditionalBlocklists,
+              CustomDNSEntries: config.ConstellationConfig.CustomDNSEntries,
+            }}
+            onSubmit={(values) => {
+              let newConfig = { ...config };
+              newConfig.ConstellationConfig.DNS = values.Enabled;
+              newConfig.ConstellationConfig.DNSPort = values.Port;
+              newConfig.ConstellationConfig.DNSFallback = values.Fallback;
+              newConfig.ConstellationConfig.DNSBlockBlacklist = values.DNSBlockBlacklist;
+              newConfig.ConstellationConfig.DNSAdditionalBlocklists = values.DNSAdditionalBlocklists;
+              newConfig.ConstellationConfig.CustomDNSEntries = values.CustomDNSEntries;
+              
+              return API.config.set(newConfig);
+            }}
+          >
+            {(formik) => (
+              <form onSubmit={formik.handleSubmit}>
+                <Stack spacing={2}>        
+                  <CosmosCheckbox formik={formik} name="Enabled" label="Constellation DNS Server Enabled" />
+                  <CosmosInputText formik={formik} name="Port" label="DNS Port" />
+                  <CosmosInputText formik={formik} name="Fallback" label="DNS Fallback" placeholder={'8.8.8.8:53'} />
+                  
+                  <CosmosFormDivider title={"DNS Blocklists"} />
+
+                  <CosmosCheckbox formik={formik} name="DNSBlockBlacklist" label="Use Blacklists to block domains" />
+
+                  <InputLabel>DNS Blocklist URLs</InputLabel>
+                  {formik.values.DNSAdditionalBlocklists.map((item, index) => (
+                    <Stack direction={"row"} spacing={2} key={`DNSAdditionalBlocklists${item}`} width={"100%"}>
+                      <DeleteButton onDelete={() => {
+                        formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
+                      }} />
+                      <div style={{flexGrow: 1}}>
+                        <CosmosInputText
+                          value={item} 
+                          name={`DNSAdditionalBlocklists${index}`}
+                          placeholder={'https://example.com/blocklist.txt'} 
+                          onChange={(e) => {
+                            formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), e.target.value, ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
+                          }}
+                        />
+                      </div>
+                    </Stack>
+                  ))}
+                  <Stack direction="row" spacing={2}>
+                    <Button variant="outlined" onClick={() => {
+                      formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists, ""]);
+                    }}>Add</Button>
+                    <Button variant="outlined" onClick={() => {
+                      formik.setFieldValue("DNSAdditionalBlocklists", [
+                        "https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
+                        "https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
+                        "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
+                        "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-only/hosts"
+                      ]);
+                    }}>Reset Default</Button>
+                  </Stack>
+
+                  <CosmosFormDivider title={"DNS Custom Entries"} />
+
+                  <InputLabel>DNS Custom Entries</InputLabel>
+                  {formik.values.CustomDNSEntries.map((item, index) => (
+                    <Stack direction={"row"} spacing={2} key={`CustomDNSEntries${item}`} width={"100%"}>
+                      <DeleteButton onDelete={() => {
+                        formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries.slice(0, index), ...formik.values.CustomDNSEntries.slice(index + 1)]);
+                      }} />
+                      <div style={{flexGrow: 1}}>
+                        <CosmosInputText
+                          value={item.Key} 
+                          name={`CustomDNSEntries${index}-key`}
+                          placeholder={'domain.com'} 
+                          onChange={(e) => {
+                            const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
+                            updatedCustomDNSEntries[index].Key = e.target.value;
+                            formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
+                          }}
+                        />
+                      </div>
+                      <div style={{flexGrow: 1}}>
+                        <CosmosInputText
+                          value={item.Value} 
+                          name={`CustomDNSEntries${index}-value`}
+                          placeholder={'1213.123.123.123'} 
+                          onChange={(e) => {
+                            const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
+                            updatedCustomDNSEntries[index].Value = e.target.value;
+                            formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
+                          }}
+                        />
+                      </div>
+                    </Stack>
+                  ))}
+                  <Stack direction="row" spacing={2}>
+                    <Button variant="outlined" onClick={() => {
+                      formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries, {
+                        Key: "",
+                        Value: "",
+                        Type: "A"
+                      }]);
+                    }}>Add</Button>
+                    <Button variant="outlined" onClick={() => {
+                      formik.setFieldValue("CustomDNSEntries", [
+                      ]);
+                    }}>Reset</Button>
+                  </Stack>
+
+                  <LoadingButton
+                      disableElevation
+                      loading={formik.isSubmitting}
+                      type="submit"
+                      variant="contained"
+                      color="primary"
+                    >
+                      Save
+                  </LoadingButton>
+                </Stack>
+              </form>
+            )}
+          </Formik>
+          </Stack>
+        </MainCard>
+      </div>  
+      </Stack>
+    </> : <center>
+      <CircularProgress color="inherit" size={20} />
+    </center>}
+  </>
+};

+ 31 - 195
client/src/pages/constellation/index.jsx

@@ -1,199 +1,35 @@
-import React from "react";
-import { useEffect, useState } from "react";
-import * as API  from "../../api";
-import AddDeviceModal from "./addDevice";
-import PrettyTableView from "../../components/tableView/prettyTableView";
-import { DeleteButton } from "../../components/delete";
-import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
-import IsLoggedIn from "../../isLoggedIn";
-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";
+import * as React from 'react';
+import MainCard from '../../components/MainCard';
+import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
+import HostChip from '../../components/hostChip';
+import { RouteMode, RouteSecurity } from '../../components/routeComponents';
+import { getFaviconURL } from '../../utils/routes';
+import * as API from '../../api';
+import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
+import IsLoggedIn from '../../isLoggedIn';
+import PrettyTabbedView from '../../components/tabbedView/tabbedView';
 
-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);
-  const [config, setConfig] = useState(null);
-  const [users, setUsers] = useState(null);
-  const [devices, setDevices] = useState(null);
-
-  const refreshConfig = async () => {
-    let configAsync = await API.config.get();
-    setConfig(configAsync.data);
-    setIsAdmin(configAsync.isAdmin);
-    setDevices((await API.constellation.list()).data || []);
-    setUsers((await API.users.list()).data || []);
-  };
+import { ConstellationVPN } from './vpn';
+import { ConstellationDNS } from './dns';
 
-  useEffect(() => {
-    refreshConfig();
-  }, []);
+const ConstellationIndex = () => {
+  return <div>
+    <IsLoggedIn />
+    
+    <PrettyTabbedView path="/cosmos-ui/constellation/:tab" tabs={[
+        {
+          title: 'VPN',
+          children: <ConstellationVPN />,
+          path: 'vpn'
+        },
+        {
+          title: 'DNS',
+          children: <ConstellationDNS />,
+          path: 'dns'
+        },
+      ]}/>
 
-  const getIcon = (r) => {
-    if (r.deviceName.toLowerCase().includes("mobile") || r.deviceName.toLowerCase().includes("phone")) {
-      return <MobileOutlined />
-    }
-    else if (r.deviceName.toLowerCase().includes("laptop") || r.deviceName.toLowerCase().includes("computer")) {
-      return <LaptopOutlined />
-    } else if (r.deviceName.toLowerCase().includes("desktop")) {
-      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 />
-    }
-  }
+  </div>;
+}
 
-  return <>
-    <IsLoggedIn />
-    {(devices && config && users) ? <>
-      <Stack spacing={2} style={{maxWidth: "1000px"}}>
-      <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}>        
-                {formik.values.Enabled && <Stack spacing={2} direction="row">    
-                  <Button
-                      disableElevation
-                      variant="outlined"
-                      color="primary"
-                      onClick={async () => {
-                        await API.constellation.restart();
-                      }}
-                    >
-                      Restart VPN Service
-                  </Button>
-                  <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}
-                      type="submit"
-                      variant="contained"
-                      color="primary"
-                    >
-                      Save
-                  </LoadingButton>
-                </Stack>
-              </form>
-            )}
-          </Formik>
-          </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} />,
-          ]}
-          columns={[
-              {
-                  title: '',
-                  field: getIcon,
-              },
-              {
-                  title: 'Device Name',
-                  field: (r) => <strong>{r.deviceName}</strong>,
-              },
-              {
-                  title: 'Owner',
-                  field: (r) => <strong>{r.nickname}</strong>,
-              },
-              {
-                  title: 'Type',
-                  field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
-              },
-              {
-                  title: 'Constellation IP',
-                  screenMin: 'md', 
-                  field: (r) => r.ip,
-              },
-              {
-                title: '',
-                clickable: true,
-                field: (r) => {
-                  return <DeleteButton onDelete={async () => {
-                    alert("caca")
-                  }}></DeleteButton>
-                }
-              }
-          ]}
-        />
-      </>}
-        </Stack>
-    </> : <center>
-      <CircularProgress color="inherit" size={20} />
-    </center>}
-  </>
-};
+export default ConstellationIndex;

+ 205 - 0
client/src/pages/constellation/vpn.jsx

@@ -0,0 +1,205 @@
+import React from "react";
+import { useEffect, useState } from "react";
+import * as API  from "../../api";
+import AddDeviceModal from "./addDevice";
+import PrettyTableView from "../../components/tableView/prettyTableView";
+import { DeleteButton } from "../../components/delete";
+import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
+import IsLoggedIn from "../../isLoggedIn";
+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 ConstellationVPN = () => {
+  const [isAdmin, setIsAdmin] = useState(false);
+  const [config, setConfig] = useState(null);
+  const [users, setUsers] = useState(null);
+  const [devices, setDevices] = useState(null);
+
+  const refreshConfig = async () => {
+    let configAsync = await API.config.get();
+    setConfig(configAsync.data);
+    setIsAdmin(configAsync.isAdmin);
+    setDevices((await API.constellation.list()).data || []);
+    setUsers((await API.users.list()).data || []);
+  };
+
+  useEffect(() => {
+    refreshConfig();
+  }, []);
+
+  const getIcon = (r) => {
+    if (r.deviceName.toLowerCase().includes("mobile") || r.deviceName.toLowerCase().includes("phone")) {
+      return <MobileOutlined />
+    }
+    else if (r.deviceName.toLowerCase().includes("laptop") || r.deviceName.toLowerCase().includes("computer")) {
+      return <LaptopOutlined />
+    } else if (r.deviceName.toLowerCase().includes("desktop")) {
+      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 />
+    }
+  }
+
+  return <>
+    <IsLoggedIn />
+    {(devices && config && users) ? <>
+      <Stack spacing={2} style={{maxWidth: "1000px"}}>
+      <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,
+              PrivateNode: config.ConstellationConfig.PrivateNode,
+              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.PrivateNode = values.PrivateNode;
+              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}>        
+                {formik.values.Enabled && <Stack spacing={2} direction="row">    
+                  <Button
+                      disableElevation
+                      variant="outlined"
+                      color="primary"
+                      onClick={async () => {
+                        await API.constellation.restart();
+                      }}
+                    >
+                      Restart VPN Service
+                  </Button>
+                  <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" />
+                      <CosmosCheckbox formik={formik} name="PrivateNode" label="This node is Private (no public IP)" />
+                      {!formik.values.PrivateNode && <>
+                        <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" />
+                      </>}
+                    </>}
+                  </>}
+                  <LoadingButton
+                      disableElevation
+                      loading={formik.isSubmitting}
+                      type="submit"
+                      variant="contained"
+                      color="primary"
+                    >
+                      Save
+                  </LoadingButton>
+                  <UploadButtons
+                    accept=".yml,.yaml"
+                    label={"Upload External Constellation Network File"}
+                    variant="outlined"
+                    fullWidth
+                    OnChange={async (e) => {
+                      let file = e.target.files[0];
+                      await API.constellation.connect(file);
+                      refreshConfig();
+                    }}
+                  />
+                </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 isAdmin={isAdmin} users={users} config={config} refreshConfig={refreshConfig} devices={devices} />,
+          ]}
+          columns={[
+              {
+                  title: '',
+                  field: getIcon,
+              },
+              {
+                  title: 'Device Name',
+                  field: (r) => <strong>{r.deviceName}</strong>,
+              },
+              {
+                  title: 'Owner',
+                  field: (r) => <strong>{r.nickname}</strong>,
+              },
+              {
+                  title: 'Type',
+                  field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
+              },
+              {
+                  title: 'Constellation IP',
+                  screenMin: 'md', 
+                  field: (r) => r.ip,
+              },
+              {
+                title: '',
+                clickable: true,
+                field: (r) => {
+                  return <DeleteButton onDelete={async () => {
+                    await API.constellation.block(r.nickname, r.deviceName, true);
+                    refreshConfig();
+                  }}></DeleteButton>
+                }
+              }
+          ]}
+        />
+      </>}
+        </Stack>
+    </> : <center>
+      <CircularProgress color="inherit" size={20} />
+    </center>}
+  </>
+};

+ 1 - 1
client/src/routes/MainRoutes.jsx

@@ -15,7 +15,7 @@ import ContainerIndex from '../pages/servapps/containers';
 import NewDockerServiceForm from '../pages/servapps/containers/newServiceForm';
 import OpenIdList from '../pages/openid/openid-list';
 import MarketPage from '../pages/market/listing';
-import { ConstellationIndex } from '../pages/constellation';
+import ConstellationIndex  from '../pages/constellation';
 
 
 // render - dashboard

+ 21 - 6
go.mod

@@ -8,7 +8,7 @@ require (
 	github.com/docker/docker v23.0.3+incompatible
 	github.com/docker/go-connections v0.4.0
 	github.com/foomo/tlsconfig v0.0.0-20180418120404-b67861b076c9
-	github.com/go-acme/lego/v4 v4.13.3
+	github.com/go-acme/lego/v4 v4.14.2
 	github.com/go-chi/chi v4.0.2+incompatible
 	github.com/go-chi/httprate v0.7.1
 	github.com/go-playground/validator/v10 v10.14.0
@@ -16,7 +16,9 @@ require (
 	github.com/gorilla/mux v1.8.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/jasonlvhit/gocron v0.0.1
+	github.com/miekg/dns v1.1.55
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
+	github.com/natefinch/lumberjack v2.0.0+incompatible
 	github.com/ory/fosite v0.44.0
 	github.com/oschwald/geoip2-golang v1.8.0
 	github.com/pquerna/otp v1.4.0
@@ -27,6 +29,7 @@ require (
 	golang.org/x/crypto v0.10.0
 	golang.org/x/net v0.11.0
 	golang.org/x/sys v0.9.0
+	gopkg.in/yaml.v2 v2.4.0
 )
 
 require (
@@ -57,7 +60,20 @@ require (
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	github.com/andybalholm/cascadia v1.1.0 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
-	github.com/aws/aws-sdk-go v1.39.0 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.19.0 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.18.28 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.13.27 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
+	github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 // indirect
+	github.com/aws/smithy-go v1.13.5 // indirect
 	github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
 	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
@@ -126,7 +142,6 @@ require (
 	github.com/magiconair/properties v1.8.4 // indirect
 	github.com/mattn/go-isatty v0.0.19 // indirect
 	github.com/mattn/goveralls v0.0.6 // indirect
-	github.com/miekg/dns v1.1.55 // indirect
 	github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -137,6 +152,7 @@ require (
 	github.com/montanaflynn/stats v0.7.0 // indirect
 	github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
 	github.com/nrdcg/auroradns v1.1.0 // indirect
+	github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 // indirect
 	github.com/nrdcg/desec v0.7.0 // indirect
 	github.com/nrdcg/dnspod-go v0.4.0 // indirect
 	github.com/nrdcg/freemyip v0.2.0 // indirect
@@ -153,7 +169,7 @@ require (
 	github.com/ory/viper v1.7.5 // indirect
 	github.com/ory/x v0.0.214 // indirect
 	github.com/oschwald/maxminddb-golang v1.10.0 // indirect
-	github.com/ovh/go-ovh v1.4.1 // indirect
+	github.com/ovh/go-ovh v1.4.2 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pborman/uuid v1.2.0 // indirect
 	github.com/pelletier/go-toml v1.8.1 // indirect
@@ -167,7 +183,6 @@ require (
 	github.com/sacloud/packages-go v0.0.9 // indirect
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17 // indirect
 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
-	github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
 	github.com/softlayer/softlayer-go v1.1.2 // indirect
@@ -210,9 +225,9 @@ require (
 	google.golang.org/grpc v1.53.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/ns1/ns1-go.v2 v2.7.6 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gotest.tools/v3 v3.4.0 // indirect
 )

+ 40 - 8
go.sum

@@ -60,6 +60,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
 github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
 github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
@@ -107,9 +108,35 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0
 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
 github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
-github.com/aws/aws-sdk-go v1.39.0 h1:74BBwkEmiqBbi2CGflEh34l0YNtIibTjZsibGarkNjo=
-github.com/aws/aws-sdk-go v1.39.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k=
+github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
+github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG1fa91q2gnqw=
+github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.27/go.mod h1:syOqAek45ZXZp29HlnRS/BNgMIW6uiRmeuQsz4Qh2UE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 h1:kP3Me6Fy3vdi+9uHd7YLr6ewPxRL+PU6y15urfTaamU=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5/go.mod h1:Gj7tm95r+QsDoN2Fhuz/3npQvcZbkEf5mL70n3Xfluc=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 h1:hMUCiE3Zi5AHrRNGf5j985u0WyqI6r2NULhUfo0N/No=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35/go.mod h1:ipR5PvpSPqIqL5Mi82BxLnfMkHVbmco8kUwO2xrCi0M=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 h1:yOpYx+FTBdpk/g+sBU6Cb1H0U/TLEcYYp66mYqsPpcc=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 h1:PwNeYoonBzmTdCztKiiutws3U24KrnDBuabzRfIlZY4=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2/go.mod h1:gQhLZrTEath4zik5ixIe6axvgY5jJrgSBDJ360Fxnco=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 h1:p4mTxJfCAyiTT4Wp6p/mOPa6j5MqCSRGot8qZwFs+Z0=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4/go.mod h1:VBLWpaHvhQNeu7N9rMEf00SWeOONb/HvaDUxe/7b44k=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k=
+github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13/go.mod h1:BzqsVVFduubEmzrVtUFQQIQdFqvUItF8XUq2EnS8Wog=
+github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 h1:e5mnydVdCVWxP+5rPAGi2PYxC7u2OZgH1ypC114H04U=
+github.com/aws/aws-sdk-go-v2/service/sts v1.19.3/go.mod h1:yVGZA1CPkmUhBdA039jXNJJG7/6t+G+EBWmFq23xqnY=
 github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04=
+github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
+github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@@ -259,8 +286,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
 github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
 github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
-github.com/go-acme/lego/v4 v4.13.3 h1:aZ1S9FXIkCWG3Uw/rZKSD+MOuO8ZB1t6p9VCg6jJiNY=
-github.com/go-acme/lego/v4 v4.13.3/go.mod h1:c/iodVGMeBXG/+KiQczoNkySo3YLWTVa0kiyeVd/FHc=
+github.com/go-acme/lego/v4 v4.14.2 h1:/D/jqRgLi8Cbk33sLGtu2pX2jEg3bGJWHyV8kFuUHGM=
+github.com/go-acme/lego/v4 v4.14.2/go.mod h1:kBXxbeTg0x9AgaOYjPSwIeJy3Y33zTz+tMD16O4MO6c=
 github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
 github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
 github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
@@ -672,6 +699,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
@@ -1024,11 +1052,15 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g=
 github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
+github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
+github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
 github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo=
 github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
+github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9 h1:qpB3wZR4+MPK92cTC9zZPnndkJgDgPvQqPUAgVc1NXU=
+github.com/nrdcg/bunny-go v0.0.0-20230728143221-c9dda82568d9/go.mod h1:HUoHXDrFvidN1NK9Wb/mZKNOfDNutKkzF2Pg71M9hHA=
 github.com/nrdcg/desec v0.7.0 h1:iuGhi4pstF3+vJWwt292Oqe2+AsSPKDynQna/eu1fDs=
 github.com/nrdcg/desec v0.7.0/go.mod h1:e1uRqqKv1mJdd5+SQROAhmy75lKMphLzWIuASLkpeFY=
 github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
@@ -1124,8 +1156,8 @@ github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6
 github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
 github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
 github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
-github.com/ovh/go-ovh v1.4.1 h1:VBGa5wMyQtTP7Zb+w97zRCh9sLtM/2YKRyy+MEJmWaM=
-github.com/ovh/go-ovh v1.4.1/go.mod h1:6bL6pPyUT7tBfI0pqOegJgRjgjuO+mOo+MyXd1EEC0M=
+github.com/ovh/go-ovh v1.4.2 h1:ub4jVK6ERbiBTo4y5wbLCjeKCjGY+K36e7BviW+MaAU=
+github.com/ovh/go-ovh v1.4.2/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY=
 github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@@ -1241,8 +1273,6 @@ github.com/shurcooL/highlight_go v0.0.0-20170515013102-78fb10f4a5f8/go.mod h1:UD
 github.com/shurcooL/octicon v0.0.0-20180602230221-c42b0e3b24d9/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
 github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04 h1:ZTzdx88+AcnjqUfJwnz89UBrMSBQ1NEysg9u5d+dU9c=
-github.com/simplesurance/bunny-go v0.0.0-20221115111006-e11d9dc91f04/go.mod h1:5KS21fpch8TIMyAUv/qQqTa3GZfBDYgjaZbd2KXKYfg=
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A=
@@ -1877,6 +1907,8 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 gopkg.in/ns1/ns1-go.v2 v2.7.6 h1:mCPl7q0jbIGACXvGBljAuuApmKZo3rRi4tlRIEbMvjA=
 gopkg.in/ns1/ns1-go.v2 v2.7.6/go.mod h1:GMnKY+ZuoJ+lVLL+78uSTjwTz2jMazq6AfGKQOYhsPk=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

+ 1 - 1
package.json

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

+ 1 - 1
readme.md

@@ -8,7 +8,7 @@
 <a href="https://github.com/soldier1"><img src="https://avatars.githubusercontent.com/soldier1" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
 <a href="https://github.com/devcircus"><img src="https://avatars.githubusercontent.com/devcircus" style="border-radius:48px" width="48" height="48" alt="Clayton Stone" title="Clayton Stone" /></a>
 <a href="https://github.com/BlackrazorNZ"><img src="https://avatars.githubusercontent.com/BlackrazorNZ" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
-<a href="https://github.com/bleibdirtroy"><img src="https://avatars.githubusercontent.com/bleibdirtroy" style="border-radius:48px" width="48" height="48" alt="Adrian Germeck" title="Adrian Germeck" /></a>
+<a href="https://github.com/bleibdirtroy"><img src="https://avatars.githubusercontent.com/bleibdirtroy" style="border-radius:48px" width="48" height="48" alt="Adrian" title="Adrian" /></a>
 </p><!-- /sponsors -->
 
 ---

+ 4 - 1
src/constellation/DNS.go

@@ -46,7 +46,10 @@ func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
 
 		// Overwrite local hostnames with custom entries
 		for _, q := range r.Question {
-			for hostname, ip := range customDNSEntries {
+			for _, entry := range customDNSEntries {
+				hostname := entry.Key
+				ip := entry.Value
+
 				if strings.HasSuffix(q.Name, hostname + ".") && q.Qtype == dns.TypeA {
 					utils.Debug("DNS Overwrite " + hostname + " with " + ip)
 					rr, _ := dns.NewRR(q.Name + " A " + ip)

+ 99 - 0
src/constellation/api_devices_block.go

@@ -0,0 +1,99 @@
+package constellation
+
+import (
+	"net/http"
+	"encoding/json"
+	
+	"github.com/azukaar/cosmos-server/src/utils" 
+)
+
+type DeviceBlockRequestJSON struct {
+	Nickname string `json:"nickname",validate:"required,min=3,max=32,alphanum"`
+	DeviceName string `json:"deviceName",validate:"required,min=3,max=32,alphanum"`
+  Block bool `json:"block",omitempty`
+}
+
+func DeviceBlock(w http.ResponseWriter, req *http.Request) {
+	if(req.Method == "POST") {
+		var request DeviceBlockRequestJSON
+		err1 := json.NewDecoder(req.Body).Decode(&request)
+		if err1 != nil {
+			utils.Error("ConstellationDeviceBlocking: Invalid User Request", err1)
+			utils.HTTPError(w, "Device Creation Error",
+				http.StatusInternalServerError, "DB001")
+			return 
+		}
+
+		errV := utils.Validate.Struct(request)
+		if errV != nil {
+			utils.Error("DeviceBlocking: Invalid User Request", errV)
+			utils.HTTPError(w, "Device Creation Error: " + errV.Error(),
+				http.StatusInternalServerError, "DB002")
+			return 
+		}
+		
+		nickname := utils.Sanitize(request.Nickname)
+		deviceName := utils.Sanitize(request.DeviceName)
+		
+		if utils.AdminOrItselfOnly(w, req, nickname) != nil {
+			return
+		}
+
+		c, errCo := utils.GetCollection(utils.GetRootAppId(), "devices")
+		if errCo != nil {
+				utils.Error("Database Connect", errCo)
+				utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
+				return
+		}
+
+		device := utils.Device{}
+
+		utils.Debug("ConstellationDeviceBlocking: Blocking Device " + deviceName)
+		
+		err2 := c.FindOne(nil, map[string]interface{}{
+			"DeviceName": deviceName,
+			"Nickname": nickname,
+			"Blocked": false,
+		}).Decode(&device)
+
+		if err2 == nil {
+			utils.Debug("ConstellationDeviceBlocking: Found Device " + deviceName)
+
+			_, err3 := c.UpdateOne(nil, map[string]interface{}{
+				"DeviceName": deviceName,
+				"Nickname": nickname,
+			}, map[string]interface{}{
+				"$set": map[string]interface{}{
+					"Blocked": request.Block,
+				},
+			})
+
+			if err3 != nil {
+				utils.Error("DeviceBlocking: Error while updating device", err3)
+				utils.HTTPError(w, "Device Creation Error: " + err3.Error(),
+					 http.StatusInternalServerError, "DB001")
+				return
+			}
+
+			if request.Block {
+				utils.Log("ConstellationDeviceBlocking: Device " + deviceName + " blocked")
+			} else {
+				utils.Log("ConstellationDeviceBlocking: Device " + deviceName + " unblocked")
+			}
+		} else {
+			utils.Error("DeviceBlocking: Error while finding device", err2)
+			utils.HTTPError(w, "Device Creation Error: " + err2.Error(),
+				 http.StatusInternalServerError, "DB001")
+			return 
+		}
+
+		
+		json.NewEncoder(w).Encode(map[string]interface{}{
+			"status": "OK",
+		})
+	} else {
+		utils.Error("DeviceBlocking: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+}

+ 11 - 2
src/constellation/api_devices_create.go

@@ -4,7 +4,7 @@ import (
 	"net/http"
 	"encoding/json"
 	"go.mongodb.org/mongo-driver/mongo"
-	
+
 	"github.com/azukaar/cosmos-server/src/utils" 
 )
 
@@ -62,11 +62,12 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
 		
 		err2 := c.FindOne(nil, map[string]interface{}{
 			"DeviceName": deviceName,
+			"Blocked": false,
 		}).Decode(&device)
 
 		if err2 == mongo.ErrNoDocuments {
 
-			cert, key, err := generateNebulaCert(deviceName, request.IP, request.PublicKey, false)
+			cert, key, fingerprint, err := generateNebulaCert(deviceName, request.IP, request.PublicKey, false)
 
 			if err != nil {
 				utils.Error("DeviceCreation: Error while creating Device", err)
@@ -82,6 +83,13 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
 				return
 			}
 
+			if err != nil {
+				utils.Error("DeviceCreation: Error while getting fingerprint", err)
+				utils.HTTPError(w, "Device Creation Error: " + err.Error(),
+					http.StatusInternalServerError, "DC007")
+				return
+			}
+
 			_, err3 := c.InsertOne(nil, map[string]interface{}{
 				"Nickname": nickname,
 				"DeviceName": deviceName,
@@ -91,6 +99,7 @@ func DeviceCreate(w http.ResponseWriter, req *http.Request) {
 				"IsRelay": request.IsRelay,
 				"PublicHostname": request.PublicHostname,
 				"Port": request.Port,
+				"Fingerprint": fingerprint,
 			})
 
 			if err3 != nil {

+ 7 - 3
src/constellation/api_nebula.go

@@ -4,7 +4,7 @@ import (
 	"net/http"
 	"encoding/json"
 	"io/ioutil"
-
+	"os"
 	
 	"github.com/azukaar/cosmos-server/src/utils" 
 )
@@ -81,11 +81,15 @@ func API_GetLogs(w http.ResponseWriter, req *http.Request) {
 	}
 
 	if(req.Method == "GET") {
-		
+		logs, err := os.ReadFile(utils.CONFIGFOLDER+"nebula.log")
+		if err != nil {
+			utils.Error("Error reading file:", err)
+			return
+		}
 		
 		json.NewEncoder(w).Encode(map[string]interface{}{
 			"status": "OK",
-			"data": logBuffer.String(),
+			"data": string(logs),
 		})
 	} else {
 		utils.Error("SettingGet: Method not allowed" + req.Method, nil)

+ 102 - 21
src/constellation/nebula.go

@@ -12,11 +12,12 @@ import (
 	"strings"
 	"io/ioutil"
 	"strconv"
+	"encoding/json"
 	"io"
-	"bytes"
+	"github.com/natefinch/lumberjack"
 )
 
-var logBuffer bytes.Buffer
+var logBuffer *lumberjack.Logger
 
 var (
 	process    *exec.Cmd
@@ -38,16 +39,24 @@ func startNebulaInBackground() error {
 		return errors.New("nebula is already running")
 	}
 
+	logBuffer = &lumberjack.Logger{
+		Filename:   utils.CONFIGFOLDER+"nebula.log",
+		MaxSize:    1, // megabytes
+		MaxBackups: 1,
+		MaxAge:     15, //days
+		Compress:   false,
+	}
+
 	process = exec.Command(binaryToRun(), "-config", utils.CONFIGFOLDER+"nebula.yml")
 
 	// Set up multi-writer for stderr
-	process.Stderr = io.MultiWriter(&logBuffer, os.Stderr)
+	process.Stderr = io.MultiWriter(logBuffer, os.Stderr)
 
 	if utils.LoggingLevelLabels[utils.GetMainConfig().LoggingLevel] == utils.DEBUG {
 		// Set up multi-writer for stdout if in debug mode
-		process.Stdout = io.MultiWriter(&logBuffer, os.Stdout)
+		process.Stdout = io.MultiWriter(logBuffer, os.Stdout)
 	} else {
-		process.Stdout = io.MultiWriter(&logBuffer)
+		process.Stdout = io.MultiWriter(logBuffer)
 	}
 
 	// Start the process in the background
@@ -125,6 +134,26 @@ func GetAllLightHouses() ([]utils.ConstellationDevice, error) {
 	return devices, nil
 }
 
+func GetBlockedDevices() ([]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{}{
+		"Blocked": 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]
 }
@@ -133,10 +162,14 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
 	// Combine defaultConfig and overwriteConfig
 	finalConfig := NebulaDefaultConfig
 
-	finalConfig.StaticHostMap = map[string][]string{
-		"192.168.201.1": []string{
-			utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
-		},
+	if !overwriteConfig.PrivateNode {
+		finalConfig.StaticHostMap = map[string][]string{
+			"192.168.201.1": []string{
+				utils.GetMainConfig().ConstellationConfig.ConstellationHostname + ":4242",
+			},
+		}
+	} else {
+		finalConfig.StaticHostMap = map[string][]string{}
 	}
 
 	// for each lighthouse
@@ -150,6 +183,16 @@ func ExportConfigToYAML(overwriteConfig utils.ConstellationConfig, outputPath st
 			l.PublicHostname + ":" + l.Port,
 		}
 	}
+
+	// add blocked devices
+	blockedDevices, err := GetBlockedDevices()
+	if err != nil {
+		return err
+	}
+
+	for _, d := range blockedDevices {
+		finalConfig.PKI.Blocklist = append(finalConfig.PKI.Blocklist, d.Fingerprint)
+	}
 	
 	// add other lighthouses 
 	finalConfig.Lighthouse.Hosts = []string{}
@@ -334,7 +377,40 @@ func killAllNebulaInstances() error {
 	return nil
 }
 
-func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, error) {
+func GetCertFingerprint(certPath string) (string, error) {
+	// nebula-cert print -json 
+	var cmd *exec.Cmd
+	
+	cmd = exec.Command(binaryToRun() + "-cert",
+		"print",
+		"-json",
+		"-path", certPath,
+	)
+
+	// capture and parse output
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		utils.Error("Error while printing cert", err)
+	}
+
+	var certInfo map[string]interface{}
+	err = json.Unmarshal(output, &certInfo)
+	if err != nil {
+		utils.Error("Error while unmarshalling cert information", err)
+		return "", err
+	}
+
+	// Extract fingerprint, replace "fingerprint" with the actual key where the fingerprint is stored in the JSON output
+	fingerprint, ok := certInfo["fingerprint"].(string)
+	if !ok {
+		utils.Error("Fingerprint not found or not a string", nil)
+		return "", errors.New("fingerprint not found or not a string")
+	}
+
+	return fingerprint, nil
+}
+
+func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, string, error) {
 	// Run the nebula-cert command
 	var cmd *exec.Cmd
 
@@ -348,9 +424,9 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, e
 		)
 	} else {
 		// write PK to temp.cert
-		err := ioutil.WriteFile("./temp.cert", []byte(PK), 0644)
+		err := ioutil.WriteFile("./temp.key", []byte(PK), 0644)
 		if err != nil {
-			return "", "", fmt.Errorf("failed to write temp.cert: %s", err)
+			return "", "", "", fmt.Errorf("failed to write temp.key: %s", err)
 		}
 		cmd = exec.Command(binaryToRun() + "-cert",
 			"sign",
@@ -358,10 +434,10 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, e
 			"-ca-key", utils.CONFIGFOLDER + "ca.key",
 			"-name", name,
 			"-ip", ip,
-			"-in-pub", "./temp.cert",
+			"-in-pub", "./temp.key",
 		)
-		// delete temp.cert
-		defer os.Remove("./temp.cert")
+		// delete temp.key
+		defer os.Remove("./temp.key")
 	}
 
 	utils.Debug(cmd.String())
@@ -377,7 +453,7 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, e
 	cmd.Run()
 
 	if cmd.ProcessState.ExitCode() != 0 {
-		return "", "", fmt.Errorf("nebula-cert exited with an error, check the Cosmos logs")
+		return "", "", "", fmt.Errorf("nebula-cert exited with an error, check the Cosmos logs")
 	}
 
 	// Read the generated certificate and key files
@@ -387,14 +463,19 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, e
 	utils.Debug("Reading certificate from " + certPath)
 	utils.Debug("Reading key from " + keyPath)
 
+	fingerprint, err := GetCertFingerprint(certPath)
+	if err != nil {
+		return "", "", "", fmt.Errorf("failed to get certificate fingerprint: %s", err)
+	}
+
 	certContent, errCert := ioutil.ReadFile(certPath)
 	if errCert != nil {
-		return "", "", fmt.Errorf("failed to read certificate file: %s", errCert)
+		return "", "", "", fmt.Errorf("failed to read certificate file: %s", errCert)
 	}
 
 	keyContent, errKey := ioutil.ReadFile(keyPath)
 	if errKey != nil {
-		return "", "", fmt.Errorf("failed to read key file: %s", errKey)
+		return "", "", "", fmt.Errorf("failed to read key file: %s", errKey)
 	}
 
 	if saveToFile {
@@ -407,15 +488,15 @@ func generateNebulaCert(name, ip, PK string, saveToFile bool) (string, string, e
 	} else {
 		// Delete the generated certificate and key files
 		if err := os.Remove(certPath); err != nil {
-			return "", "", fmt.Errorf("failed to delete certificate file: %s", err)
+			return "", "", "", fmt.Errorf("failed to delete certificate file: %s", err)
 		}
 
 		if err := os.Remove(keyPath); err != nil {
-			return "", "", fmt.Errorf("failed to delete key file: %s", err)
+			return "", "", "", fmt.Errorf("failed to delete key file: %s", err)
 		}
 	}
 
-	return string(certContent), string(keyContent), nil
+	return string(certContent), string(keyContent), fingerprint, nil
 }
 
 func generateNebulaCACert(name string) (error) {

+ 2 - 0
src/constellation/nebula_default.go

@@ -12,10 +12,12 @@ func InitConfig() {
 			CA   string `yaml:"ca"`
 			Cert string `yaml:"cert"`
 			Key  string `yaml:"key"`
+			Blocklist []string `yaml:"blocklist"`
 		}{
 			CA:   utils.CONFIGFOLDER + "ca.crt",
 			Cert: utils.CONFIGFOLDER + "cosmos.crt",
 			Key:  utils.CONFIGFOLDER + "cosmos.key",
+			Blocklist: []string{},
 		},
 		StaticHostMap: map[string][]string{
 			

+ 1 - 0
src/httpServer.go

@@ -338,6 +338,7 @@ func InitServer() *mux.Router {
 	srapi.HandleFunc("/api/constellation/connect", constellation.API_ConnectToExisting)
 	srapi.HandleFunc("/api/constellation/config", constellation.API_GetConfig)
 	srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
+	srapi.HandleFunc("/api/constellation/block", constellation.DeviceBlock)
 
 	if(!config.HTTPConfig.AcceptAllInsecureHostname) {
 		srapi.Use(utils.EnsureHostname)

+ 10 - 1
src/utils/types.go

@@ -212,16 +212,22 @@ type MarketSource struct {
 type ConstellationConfig struct {
 	Enabled bool
 	SlaveMode bool
+	PrivateNode bool
 	DNS bool
 	DNSPort string
 	DNSFallback string
 	DNSBlockBlacklist bool
 	DNSAdditionalBlocklists []string
-	CustomDNSEntries map[string]string
+	CustomDNSEntries []ConstellationDNSEntry
 	NebulaConfig NebulaConfig
 	ConstellationHostname string
 }
 
+type ConstellationDNSEntry struct {
+	Type string
+	Key string
+	Value string
+}
 type ConstellationDevice struct {
 	Nickname string `json:"nickname"`
 	DeviceName string `json:"deviceName"`
@@ -231,6 +237,8 @@ type ConstellationDevice struct {
 	IsRelay bool `json:"isRelay"`
 	PublicHostname string `json:"publicHostname"`
 	Port string `json:"port"`
+	Blocked bool `json:"blocked"`
+	Fingerprint string `json:"fingerprint"`
 }
 
 type NebulaFirewallRule struct {
@@ -251,6 +259,7 @@ type NebulaConfig struct {
 		CA   string `yaml:"ca"`
 		Cert string `yaml:"cert"`
 		Key  string `yaml:"key"`
+		Blocklist []string `yaml:"blocklist"`
 	} `yaml:"pki"`
 
 	StaticHostMap map[string][]string `yaml:"static_host_map"`