mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-07 17:18:21 +00:00
Merge branch 'main' into feat-custom-multiselect
This commit is contained in:
commit
1b2a1ec6e1
75 changed files with 3050 additions and 969 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,5 +1,9 @@
|
|||
config.json
|
||||
pending.json
|
||||
nftables.conf
|
||||
interfaces.conf
|
||||
go.work
|
||||
nfsense
|
||||
nfsense.exe
|
||||
nfsense.exe
|
||||
out/*
|
||||
out
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^0.8.2",
|
||||
"@vee-validate/zod": "^4.8.4",
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"@vueuse/head": "^1.1.15",
|
||||
"axios": "^1.3.4",
|
||||
|
@ -25,7 +26,9 @@
|
|||
"vue": "^3.2.45",
|
||||
"vue-i18n": "9",
|
||||
"vue-router": "4",
|
||||
"ws": "^8.13.0"
|
||||
"vue-toast-notification": "^3.0",
|
||||
"ws": "^8.13.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.30",
|
||||
|
|
1380
client/pnpm-lock.yaml
generated
1380
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -5,9 +5,12 @@ import { authenticate, logout, checkAuthentication, setup } from "./api";
|
|||
import IDashboard from '~icons/ri/dashboard-2-line';
|
||||
import IRule from '~icons/material-symbols/rule-folder-outline-sharp';
|
||||
import IAddress from '~icons/eos-icons/ip';
|
||||
import IEthernet from '~icons/bi/ethernet';
|
||||
import IService from '~icons/material-symbols/home-repair-service';
|
||||
import ISNAT from '~icons/mdi/arrow-expand-right';
|
||||
import IDNAT from '~icons/mdi/arrow-collapse-right';
|
||||
import IConfig from '~icons/grommet-icons/document-config';
|
||||
import IStaticRoutes from '~icons/material-symbols/drive-folder-upload-outline-sharp';
|
||||
|
||||
enum NavState { Open, Reduced, Collapsed };
|
||||
const NavStateCount = 3;
|
||||
|
@ -17,8 +20,11 @@ const navRoutes = {
|
|||
"/firewall/forwardrules": { icon: IRule, caption: "Rules" },
|
||||
"/firewall/sourcenatrules": { icon: ISNAT, caption: "SNAT" },
|
||||
"/firewall/destinationnatrules": { icon: IDNAT, caption: "DNAT" },
|
||||
"/network/interfaces": { icon: IEthernet, caption: "Interfaces" },
|
||||
"/network/staticroutes": { icon: IStaticRoutes, caption: "Static Routes" },
|
||||
"/object/addresses": { icon: IAddress, caption: "Addresses" },
|
||||
"/object/services": { icon: IService, caption: "Services" },
|
||||
"/config/config": { icon: IConfig, caption: "Config" },
|
||||
};
|
||||
|
||||
enum AuthState { Unauthenticated, MfaRequired, Authenticated };
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// import WebSocketServer from 'ws';
|
||||
import JsonRPC from 'simple-jsonrpc-js';
|
||||
import axios from "axios";
|
||||
import { useToast } from 'vue-toast-notification';
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
let jrpc = new JsonRPC.connect_xhr('/api');
|
||||
// let socket = new WebSocket("ws://"+ window.location.host +"/ws/api");
|
||||
|
@ -21,6 +24,7 @@ export async function apiCall(method: string, params: Record<string, any>): Prom
|
|||
if (ex.code === 401) {
|
||||
UnauthorizedCallback();
|
||||
} else {
|
||||
$toast.error(method+ ': ' + ex.message);
|
||||
console.debug("api call epic fail", ex);
|
||||
}
|
||||
return { Data: null, Error: ex};
|
||||
|
|
87
client/src/components/NiceForm.vue
Normal file
87
client/src/components/NiceForm.vue
Normal file
|
@ -0,0 +1,87 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
title: string
|
||||
validationSchema: Record<string, string | Function>,
|
||||
sections: {
|
||||
title: string
|
||||
fields: {
|
||||
key: string,
|
||||
label: string,
|
||||
as: string,
|
||||
props: any,
|
||||
default: any,
|
||||
enabled?: (values: Record<string, any>) => Boolean,
|
||||
rules?: (value: any) => true | string,
|
||||
}[],
|
||||
}[],
|
||||
modelValue: any,
|
||||
submit: (value: any) => boolean,
|
||||
discard: () => void,
|
||||
}>();
|
||||
|
||||
let { sections, submit, discard, validationSchema } = $(props);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ValidationForm as="div" v-slot="{ values, handleSubmit }" @submit="submit" :validationSchema="validationSchema">
|
||||
<template v-for="(section, index) in sections" :key="index">
|
||||
<h4 v-if="section.title">{{ section.title }}</h4>
|
||||
<div class="section">
|
||||
<template v-for="(field, index) in section.fields" :key="index">
|
||||
<template v-if="field.enabled ? field.enabled(values) : true">
|
||||
<label :for="field.key" v-text="field.label" />
|
||||
<Field v-if="field.as == 'NumberBox'" :name="field.key" :as="field.as" :rules="field.rules" v-bind="field.props" @update:modelValue="values[field.key] = Number(values[field.key])"/>
|
||||
<Field v-else :name="field.key" :as="field.as" :rules="field.rules" v-bind="field.props"/>
|
||||
<ErrorMessage :name="field.key" />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="actions">
|
||||
<div class="flex-grow"/>
|
||||
<button @click="handleSubmit($event, submit)">Submit</button>
|
||||
<div class="space"/>
|
||||
<button @click="discard">Discard</button>
|
||||
<div class="flex-grow"/>
|
||||
</div>
|
||||
<p>{{ values }}</p>
|
||||
</ValidationForm>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.section {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
padding: 0.5rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.actions {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.space {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.actions > button {
|
||||
flex-grow: true;
|
||||
}
|
||||
|
||||
h4,
|
||||
p {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
h4 {
|
||||
background-color: var(--cl-bg-hl);
|
||||
padding: 0.3rem;
|
||||
padding-left: 0.5rem;
|
||||
;
|
||||
}
|
||||
</style>
|
|
@ -15,15 +15,20 @@ const props = defineModel<{
|
|||
sortSelf?: boolean,
|
||||
sortBy?: string,
|
||||
sortDesc?: boolean,
|
||||
selection?: number[],
|
||||
draggable?: boolean,
|
||||
}>();
|
||||
let { columns, data, sort, sortSelf, sortBy, sortDesc } = $(props);
|
||||
let { columns, data, sort, sortSelf, sortBy, sortDesc, selection, draggable } = $(props);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'rowAction', index: number): void,
|
||||
(event: 'selectionChanged'): void
|
||||
(event: 'draggedRow', draggedRow: number, draggedOverRow: number): void,
|
||||
}>();
|
||||
|
||||
let selection = $ref([] as number[]);
|
||||
if (selection == undefined) {
|
||||
selection = [];
|
||||
}
|
||||
|
||||
const displayData = $computed(() => (sortSelf && sortBy !== '')
|
||||
? data?.sort((a, b) => {
|
||||
|
@ -97,7 +102,9 @@ function dragDropRow() {
|
|||
data.splice(draggedRow, 1);
|
||||
data.splice(draggedOverRow, 0, row);
|
||||
data = data;
|
||||
emit("draggedRow", draggedRow, draggedOverRow);
|
||||
}
|
||||
|
||||
// Reset drag data
|
||||
draggedRow = 0;
|
||||
draggedOverRow = 0;
|
||||
|
@ -123,7 +130,7 @@ function dragDropRow() {
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in displayData" :key="index"
|
||||
draggable="true"
|
||||
:draggable="draggable"
|
||||
@click="() => rowSelection(index)"
|
||||
@dblclick="() => emit('rowAction', index)"
|
||||
@dragstart="() => draggedRow = index"
|
||||
|
|
36
client/src/components/TableView.vue
Normal file
36
client/src/components/TableView.vue
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
title: string,
|
||||
loading: boolean,
|
||||
columns?: {
|
||||
heading: string,
|
||||
path: string,
|
||||
component?: Component,
|
||||
}[],
|
||||
data: Record<string, any>[],
|
||||
tableProps: any,
|
||||
selection?: number[],
|
||||
}>();
|
||||
|
||||
let { title, loading, columns, data, selection, tableProps } = $(props);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'draggedRow', draggedRow: number, draggedOverRow: number): void,
|
||||
}>();
|
||||
|
||||
async function draggedRow(draggedRow: number, draggedOverRow: number) {
|
||||
emit("draggedRow", draggedRow, draggedOverRow);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader :title="title">
|
||||
<slot/>
|
||||
</PageHeader>
|
||||
<div v-if="loading" >Loading...</div>
|
||||
<NiceTable v-else :columns="columns" v-model:selection="selection" @draggedRow="draggedRow" v-bind="tableProps" :data="data"/>
|
||||
</div>
|
||||
</template>
|
21
client/src/components/inputs/CheckBox.vue
Normal file
21
client/src/components/inputs/CheckBox.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
modelValue: boolean,
|
||||
}>();
|
||||
let { modelValue } = $(props);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="() => modelValue = !modelValue">
|
||||
<i-material-symbols-check-box-outline v-if="modelValue"/>
|
||||
<i-material-symbols-check-box-outline-blank v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
18
client/src/components/inputs/MultilineTextBox.vue
Normal file
18
client/src/components/inputs/MultilineTextBox.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
modelValue: string,
|
||||
}>();
|
||||
let { modelValue } = $(props);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea v-model="modelValue" rows="5"/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
</style>
|
17
client/src/components/inputs/NumberBox.vue
Normal file
17
client/src/components/inputs/NumberBox.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
modelValue: number,
|
||||
min?: number,
|
||||
max?: number,
|
||||
}>();
|
||||
let { modelValue, min, max } = $(props);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input type="number" v-model.number="modelValue" :min="min" :max="max">
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
51
client/src/components/inputs/PillBar.vue
Normal file
51
client/src/components/inputs/PillBar.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
options: {
|
||||
name: string,
|
||||
key: string,
|
||||
icon: Component,
|
||||
}[],
|
||||
modelValue: number | string,
|
||||
useIndex: boolean,
|
||||
}>();
|
||||
let { options, modelValue, useIndex } = $(props);
|
||||
|
||||
function setSelection(option: any, index: number){
|
||||
if (useIndex) {
|
||||
modelValue = index
|
||||
} else {
|
||||
modelValue = option.key
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
if (modelValue === undefined) {
|
||||
if (useIndex) {
|
||||
modelValue = 0
|
||||
} else {
|
||||
modelValue = options[0].key
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button class="option" v-for="(option, index) in options" :key="index" :class="{selected: modelValue == index || modelValue == option.key}" @click="setSelection(option, index)">
|
||||
<i class="material-icons" v-if="option.icon">{{ option.icon }}</i>
|
||||
{{ option.name }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
flex-flow: nowrap;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--cl-bg-sl);
|
||||
}
|
||||
</style>
|
15
client/src/components/inputs/TextBox.vue
Normal file
15
client/src/components/inputs/TextBox.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
modelValue: string,
|
||||
}>();
|
||||
let { modelValue } = $(props);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-model="modelValue">
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -1,44 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
const props = defineModel<{
|
||||
options: {
|
||||
name: string,
|
||||
icon: Component,
|
||||
selected: boolean,
|
||||
}[],
|
||||
}>();
|
||||
let { options } = $(props);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'selectionChanged'): void
|
||||
}>();
|
||||
|
||||
function select(option: any) {
|
||||
for(let opt of options) {
|
||||
opt.selected = false;
|
||||
}
|
||||
option.selected = true;
|
||||
emit('selectionChanged');
|
||||
console.debug("selected", options);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<button class="option" v-for="(option, index) in options" :key="index" :class="{selected:option.selected}" @click="select(option)">
|
||||
<i class="material-icons" v-if="option.icon">{{ option.icon }}</i>
|
||||
{{ option.name }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
flex-flow: nowrap;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--cl-bg-sl);
|
||||
}
|
||||
</style>
|
84
client/src/definitions.ts
Normal file
84
client/src/definitions.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { toFormValidator } from '@vee-validate/zod';
|
||||
import * as zod from 'zod';
|
||||
|
||||
export const editTypes: { [key: string]: { [key: string]: any } } = {
|
||||
"firewall": {
|
||||
name: "Firewall",
|
||||
"forwardrules": {
|
||||
name: "ForwardRule",
|
||||
validationSchema: toFormValidator(
|
||||
zod.object({
|
||||
name: zod.string(),
|
||||
verdict: zod.string(),
|
||||
counter: zod.boolean(),
|
||||
comment: zod.string().optional(),
|
||||
}),
|
||||
),
|
||||
sections: [
|
||||
{
|
||||
fields: [
|
||||
{ key: "name", label: "Name", as: "TextBox" },
|
||||
{ key: "verdict", label: "Verdict", as: "PillBar", props: { options: [{ name: 'Accept', key: 'accept' }, { name: 'Drop', key: 'drop' }, { name: 'Continue', key: 'continue' }] } },
|
||||
{ key: "counter", label: "Counter", as: "CheckBox", },
|
||||
{ key: "comment", label: "Comment", as: "MultilineTextBox", },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"network": {
|
||||
name: "Network",
|
||||
"interfaces": {
|
||||
name: "Interface",
|
||||
validationSchema: toFormValidator(
|
||||
zod.object({
|
||||
name: zod.string(),
|
||||
type: zod.string(),
|
||||
hardware_interface: zod.string().optional(),
|
||||
vlan_id: zod.number().optional(),
|
||||
comment: zod.string().optional(),
|
||||
}),
|
||||
),
|
||||
sections: [
|
||||
{
|
||||
fields: [
|
||||
{ key: "name", label: "Name", as: "TextBox", default: "placeholder" },
|
||||
{ key: "type", label: "Type", as: "PillBar", props: { options: [{ name: 'Hardware', key: 'hardware' }, { name: 'VLAN', key: 'vlan' }, { name: 'Bond', key: 'bond' }, { name: 'Bridge', key: 'bridge' }] } },
|
||||
{ key: "hardware_device", label: "Hardware Device", as: "TextBox", enabled: (values: any) => (values["type"] == 'hardware') },
|
||||
{ key: "vlan_parent", label: "VLAN Parent", as: "TextBox", enabled: (values: any) => (values["type"] == 'vlan') },
|
||||
{ key: "vlan_id", label: "VLAN ID", as: "NumberBox", props: { min: 1, max: 4094 }, enabled: (values: any) => (values["type"] == 'vlan') },
|
||||
{ key: "bond_members", label: "Bond Members", as: "TextBox", enabled: (values: any) => (values["type"] == 'bond') },
|
||||
{ key: "bridge_members", label: "Bridge Members", as: "TextBox", enabled: (values: any) => (values["type"] == 'bridge') },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Addressing",
|
||||
fields: [
|
||||
{ key: "addressing_mode", label: "Addressing Mode", as: "PillBar", props: { options: [{ name: 'None', key: 'none' }, { name: 'Static', key: 'static' }, { name: 'DHCP', key: 'dhcp' }] } },
|
||||
{ key: "address", label: "Address", as: "TextBox", enabled: (values: any) => (values["addressing_mode"] == 'static') },
|
||||
{ key: "comment", label: "Comment", as: "MultilineTextBox" },
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
"staticroutes": {
|
||||
name: "StaticRoute",
|
||||
validationSchema: toFormValidator(
|
||||
zod.object({
|
||||
name: zod.string(),
|
||||
}),
|
||||
),
|
||||
sections: [
|
||||
{
|
||||
fields: [
|
||||
{ key: "name", label: "Name", as: "TextBox", },
|
||||
{ key: "interface", label: "Interface", as: "TextBox" },
|
||||
{ key: "gateway", label: "Gateway", as: "TextBox" },
|
||||
{ key: "destination", label: "Destination", as: "TextBox" },
|
||||
{ key: "metric", label: "Metric", as: "NumberBox" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -3,4 +3,5 @@
|
|||
.pad { padding: 0.5rem; }
|
||||
.gap { gap: 0.5rem; }
|
||||
.flex-grow { flex-grow: 1; }
|
||||
.flex-row { flex-direction: row; }
|
||||
.flex-row { flex-direction: row; }
|
||||
.scroll { overflow-y:auto; }
|
|
@ -47,7 +47,7 @@ th:hover {
|
|||
}
|
||||
th, td {
|
||||
padding: 0.5rem;
|
||||
border: 0.125rem solid var(--cl-fg);
|
||||
border: 0.125rem solid var(--cl-bg-el);
|
||||
}
|
||||
th > *{
|
||||
justify-content: center;
|
||||
|
@ -77,6 +77,11 @@ button, .button {
|
|||
background-color: var(--cl-bg-hl);
|
||||
}
|
||||
|
||||
.button:disabled, button:disabled, .disabled {
|
||||
background-color: var(--cl-bg-hl);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: var(--cl-bg-hl);
|
||||
border: 1px solid var(--cl-fg);
|
||||
|
|
|
@ -3,11 +3,21 @@ import './global-styles/components.css';
|
|||
import './global-styles/colors.css';
|
||||
import './global-styles/mlfe.css';
|
||||
import './global-styles/transitions.css';
|
||||
import 'vue-toast-notification/dist/theme-default.css';
|
||||
|
||||
import PillBar from "./components/inputs/PillBar.vue";
|
||||
import TextBox from "./components/inputs/TextBox.vue";
|
||||
import NumberBox from "./components/inputs/NumberBox.vue";
|
||||
import MultilineTextBox from "./components/inputs/MultilineTextBox.vue";
|
||||
import CheckBox from "./components/inputs/CheckBox.vue";
|
||||
|
||||
import { Form, Field, ErrorMessage } from 'vee-validate';
|
||||
|
||||
import App from './App.vue';
|
||||
import { createHead } from '@vueuse/head';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import routes from '~pages';
|
||||
import ToastPlugin from 'vue-toast-notification';
|
||||
|
||||
const app = createApp(App);
|
||||
const head = createHead();
|
||||
|
@ -18,5 +28,16 @@ const router = createRouter({
|
|||
|
||||
app.use(router);
|
||||
app.use(head);
|
||||
app.use(ToastPlugin);
|
||||
|
||||
// Global Components
|
||||
app.component('PillBar', PillBar);
|
||||
app.component('TextBox', TextBox);
|
||||
app.component('NumberBox', NumberBox);
|
||||
app.component('MultilineTextBox', MultilineTextBox);
|
||||
app.component('CheckBox', CheckBox);
|
||||
app.component('ValidationForm', Form);
|
||||
app.component('Field', Field);
|
||||
app.component('ErrorMessage', ErrorMessage);
|
||||
|
||||
app.mount('#app');
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
const props = $defineProps<{entity: string, id: string}>();
|
||||
const { entity, id } = $(props);
|
||||
|
||||
const pageTypes: { [key: string]: any } = {
|
||||
"rules": { title: "Rules" },
|
||||
"addresses": { title: "Addresses"},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader :title="pageTypes[entity].title"/>
|
||||
{{ entity }} {{ id }}
|
||||
</div>
|
||||
</template>
|
|
@ -1,90 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
const props = $defineProps<{entity: string}>();
|
||||
const { entity } = $(props);
|
||||
|
||||
const pageTypes: { [key: string]: any } = {
|
||||
"rules": { title: "Rules" },
|
||||
"addresses": { title: "Addresses"},
|
||||
};
|
||||
|
||||
let searchTerm = $ref("");
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader :title="pageTypes[entity].title">
|
||||
<input class="search-bar" placeholder="Search..." v-model="searchTerm"/>
|
||||
<button>
|
||||
<i-material-symbols-add/>
|
||||
</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="{fname: 'First Name', lname: 'Last Name'}" :sort-self="true" :data="[
|
||||
{
|
||||
fname: 'Haynes',
|
||||
lname: 'Chavez'
|
||||
}, {
|
||||
fname: 'Brennan',
|
||||
lname: 'Bradley'
|
||||
}, {
|
||||
fname: 'Blanchard',
|
||||
lname: 'Thornton'
|
||||
}, {
|
||||
fname: 'Benjamin',
|
||||
lname: 'Nash'
|
||||
}, {
|
||||
fname: 'Jan',
|
||||
lname: 'Bradford'
|
||||
}, {
|
||||
fname: 'Zelma',
|
||||
lname: 'Spears'
|
||||
}, {
|
||||
fname: 'Freeman',
|
||||
lname: 'Page'
|
||||
}, {
|
||||
fname: 'Wilson',
|
||||
lname: 'Carlson'
|
||||
}, {
|
||||
fname: 'Lewis',
|
||||
lname: 'Fuentes'
|
||||
}, {
|
||||
fname: 'Vega',
|
||||
lname: 'Villarreal'
|
||||
}, {
|
||||
fname: 'Carolyn',
|
||||
lname: 'Cardenas'
|
||||
}, {
|
||||
fname: 'Angie',
|
||||
lname: 'Adams'
|
||||
}, {
|
||||
fname: 'Richards',
|
||||
lname: 'Leon'
|
||||
}, {
|
||||
fname: 'Velma',
|
||||
lname: 'Fields'
|
||||
}, {
|
||||
fname: 'Witt',
|
||||
lname: 'Lowe'
|
||||
}, {
|
||||
fname: 'Waters',
|
||||
lname: 'Leblanc'
|
||||
}, {
|
||||
fname: 'Henry',
|
||||
lname: 'Lloyd'
|
||||
}, {
|
||||
fname: 'Boone',
|
||||
lname: 'Greer'
|
||||
}, {
|
||||
fname: 'Willis',
|
||||
lname: 'Stark'
|
||||
}, {
|
||||
fname: 'Dickson',
|
||||
lname: 'Spencer'
|
||||
}
|
||||
].filter(x => (`${x.fname} ${x.lname}`).toLowerCase().includes(searchTerm.toLowerCase()))"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
29
client/src/pages/[subsystem]/[entity]/edit/[id].vue
Normal file
29
client/src/pages/[subsystem]/[entity]/edit/[id].vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { editTypes } from "../../../../definitions";
|
||||
import getPlugins from '../../../../plugins';
|
||||
const p = getPlugins();
|
||||
|
||||
const props = $defineProps<{subsystem: string, entity: string, id: string}>();
|
||||
const { subsystem, entity, id } = $(props);
|
||||
|
||||
let data = $ref({} as {});
|
||||
|
||||
async function update() {
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="editTypes[subsystem][entity]">
|
||||
<PageHeader :title="'Edit ' + editTypes[subsystem][entity].name">
|
||||
<button @click="update">Update</button>
|
||||
<button @click="$router.go(-1)">Discard</button>
|
||||
</PageHeader>
|
||||
<NiceForm class="scroll cl-secondary" :sections="editTypes[subsystem][entity].sections" v-model="data"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<PageHeader title="Error"/>
|
||||
No editType for this Entity
|
||||
</div>
|
||||
</template>
|
34
client/src/pages/[subsystem]/[entity]/edit/index.vue
Normal file
34
client/src/pages/[subsystem]/[entity]/edit/index.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { editTypes } from "../../../../definitions";
|
||||
import { apiCall } from "../../../../api";
|
||||
import getPlugins from '../../../../plugins';
|
||||
const p = getPlugins();
|
||||
|
||||
const props = $defineProps<{subsystem: string, entity: string}>();
|
||||
const { subsystem, entity } = $(props);
|
||||
|
||||
let data = $ref({});
|
||||
|
||||
async function create(value: any) {
|
||||
console.debug("value", value);
|
||||
let res = await apiCall(editTypes[subsystem].name +".Create"+ editTypes[subsystem][entity].name, value);
|
||||
if (res.Error === null) {
|
||||
p.toast.success("Created " + editTypes[subsystem][entity].name);
|
||||
p.router.go(-1);
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div v-if="editTypes[subsystem][entity]">
|
||||
<PageHeader :title="'Create ' + editTypes[subsystem][entity].name">
|
||||
</PageHeader>
|
||||
<NiceForm class="scroll cl-secondary" :submit="create" :discard="() => $router.go(-1)" :sections="editTypes[subsystem][entity].sections" v-model="data"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<PageHeader title="Error"/>
|
||||
No editType for this Entity
|
||||
</div>
|
||||
</template>
|
77
client/src/pages/config/config.vue
Normal file
77
client/src/pages/config/config.vue
Normal file
|
@ -0,0 +1,77 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
import getPlugins from '../../plugins';
|
||||
const p = getPlugins();
|
||||
|
||||
|
||||
let changelog = $ref([]);
|
||||
let loading = $ref(false);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Path', path: 'path'},
|
||||
{heading: 'Type', path: 'type'},
|
||||
{heading: 'From', path: 'from'},
|
||||
{heading: 'To', path: 'to'},
|
||||
];
|
||||
|
||||
const displayData = $computed(() => {
|
||||
let data: any;
|
||||
data = [];
|
||||
for (const change of changelog) {
|
||||
data.push({
|
||||
path: change.path,
|
||||
type: change.type,
|
||||
from: change.from,
|
||||
to: change.to,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
async function load(){
|
||||
loading = true
|
||||
let res = await apiCall("Config.GetPendingChangelog", {});
|
||||
if (res.Error === null) {
|
||||
console.debug("changelog", res.Data.Changelog);
|
||||
changelog = res.Data.Changelog;
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function apply(){
|
||||
let res = await apiCall("Config.ApplyPendingChanges", {});
|
||||
if (res.Error === null) {
|
||||
console.debug("apply");
|
||||
p.toast.success("Applied Pending Config");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
async function discard(){
|
||||
let res = await apiCall("Config.DiscardPendingChanges", {});
|
||||
if (res.Error === null) {
|
||||
console.debug("discard");
|
||||
p.toast.success("Discarded Pending Config");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
load();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableView title="Pending Changes" :columns="columns" :loading="loading" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<button @click="apply">Apply</button>
|
||||
<button @click="discard">Discard</button>
|
||||
</TableView>
|
||||
</template>
|
|
@ -1,38 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
import getPlugins from '../../plugins';
|
||||
const p = getPlugins();
|
||||
|
||||
let rules = $ref([]);
|
||||
let loading = $ref(false);
|
||||
let selection = $ref([] as number[]);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Name', path: 'name'},
|
||||
{heading: 'Source', path: 'match.source_addresses'},
|
||||
{heading: 'Destination', path: 'match.destination_addresses'},
|
||||
{heading: 'Service', path: 'match.services'},
|
||||
{heading: 'Verdict', path: 'verdict'},
|
||||
{heading: 'Counter', path: 'counter'},
|
||||
{heading: 'Comment', path: 'comment'},
|
||||
];
|
||||
|
||||
async function loadRules(){
|
||||
async function load(){
|
||||
let res = await apiCall("Firewall.GetDestinationNATRules", {});
|
||||
if (res.Error === null) {
|
||||
rules = res.Data.DestinationNATRules;
|
||||
rules = res.Data.destination_nat_rules;
|
||||
console.debug("rules", rules);
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRule(){
|
||||
let res = await apiCall("Firewall.DeleteDestinationNATRule", {index: selection[0]});
|
||||
if (res.Error === null) {
|
||||
console.debug("deleted rule");
|
||||
p.toast.success("Deleted Rule");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
async function draggedRow(draggedRow: number, draggedOverRow: number) {
|
||||
console.log("dragged", draggedRow, draggedOverRow);
|
||||
let res = await apiCall("Firewall.MoveDestinationNATRule", {index: draggedRow, to_index: draggedOverRow});
|
||||
if (res.Error === null) {
|
||||
console.debug("moved rule");
|
||||
p.toast.success("Moved Rule");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
loadRules();
|
||||
load();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="DNAT Rules">
|
||||
<button @click="loadRules">Load Rules</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="columns" v-model:data="rules"/>
|
||||
<TableView title="DNAT Rules" :columns="columns" :loading="loading" @draggedRow="draggedRow" v-model:selection="selection" v-model:data="rules" :table-props="{sort:true, sortSelf: true, draggable: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<router-link class="button" to="/firewall/destinationnatrules/edit">Create</router-link>
|
||||
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/firewall/destinationnatrules/edit/' + selection[0]">Edit</router-link>
|
||||
<button @click="deleteRule" :disabled="selection.length != 1">Delete</button>
|
||||
</TableView>
|
||||
</div>
|
||||
</template>
|
|
@ -1,7 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
import getPlugins from '../../plugins';
|
||||
const p = getPlugins();
|
||||
|
||||
let rules = $ref([]);
|
||||
let loading = $ref(false);
|
||||
let selection = $ref([] as number[]);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Name', path: 'name'},
|
||||
{heading: 'Source', path: 'match.source_addresses'},
|
||||
|
@ -12,27 +17,52 @@ const columns = [
|
|||
{heading: 'Comment', path: 'comment'},
|
||||
];
|
||||
|
||||
async function loadRules(){
|
||||
async function load(){
|
||||
let res = await apiCall("Firewall.GetForwardRules", {});
|
||||
if (res.Error === null) {
|
||||
rules = res.Data.ForwardRules;
|
||||
rules = res.Data.forward_rules;
|
||||
console.debug("rules", rules);
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRule(){
|
||||
let res = await apiCall("Firewall.DeleteForwardRule", {index: selection[0]});
|
||||
if (res.Error === null) {
|
||||
console.debug("deleted rule");
|
||||
p.toast.success("Deleted Rule");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
async function draggedRow(draggedRow: number, draggedOverRow: number) {
|
||||
console.log("dragged", draggedRow, draggedOverRow);
|
||||
let res = await apiCall("Firewall.MoveForwardRule", {index: draggedRow, to_index: draggedOverRow});
|
||||
if (res.Error === null) {
|
||||
console.debug("moved rule");
|
||||
p.toast.success("Moved Rule");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
loadRules();
|
||||
load();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="Forward Rules">
|
||||
<button @click="loadRules">Load Rules</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="columns" v-model:data="rules"/>
|
||||
<TableView title="Forward Rules" :columns="columns" :loading="loading" @draggedRow="draggedRow" v-model:selection="selection" v-model:data="rules" :table-props="{sort:true, sortSelf: true, draggable: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<router-link class="button" to="/firewall/forwardrules/edit">Create</router-link>
|
||||
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/firewall/forwardrules/edit/' + selection[0]">Edit</router-link>
|
||||
<button @click="deleteRule" :disabled="selection.length != 1">Delete</button>
|
||||
</TableView>
|
||||
</div>
|
||||
</template>
|
|
@ -1,38 +1,67 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
import getPlugins from '../../plugins';
|
||||
const p = getPlugins();
|
||||
|
||||
let rules = $ref([]);
|
||||
let loading = $ref(false);
|
||||
let selection = $ref([] as number[]);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Name', path: 'name'},
|
||||
{heading: 'Source', path: 'match.source_addresses'},
|
||||
{heading: 'Destination', path: 'match.destination_addresses'},
|
||||
{heading: 'Service', path: 'match.services'},
|
||||
{heading: 'Verdict', path: 'verdict'},
|
||||
{heading: 'Counter', path: 'counter'},
|
||||
{heading: 'Comment', path: 'comment'},
|
||||
];
|
||||
|
||||
async function loadRules(){
|
||||
async function load(){
|
||||
let res = await apiCall("Firewall.GetSourceNATRules", {});
|
||||
if (res.Error === null) {
|
||||
rules = res.Data.SourceNATRules;
|
||||
rules = res.Data.source_nat_rules;
|
||||
console.debug("rules", rules);
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRule(){
|
||||
let res = await apiCall("Firewall.DeleteSourceNATRule", {index: selection[0]});
|
||||
if (res.Error === null) {
|
||||
console.debug("deleted rule");
|
||||
p.toast.success("Deleted Rule");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
async function draggedRow(draggedRow: number, draggedOverRow: number) {
|
||||
console.log("dragged", draggedRow, draggedOverRow);
|
||||
let res = await apiCall("Firewall.MoveSourceNATRule", {index: draggedRow, to_index: draggedOverRow});
|
||||
if (res.Error === null) {
|
||||
console.debug("moved rule");
|
||||
p.toast.success("Moved Rule");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
loadRules();
|
||||
load();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="SNAT Rules">
|
||||
<button @click="loadRules">Load Rules</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="columns" v-model:data="rules"/>
|
||||
<TableView title="SNAT Rules" :columns="columns" :loading="loading" @draggedRow="draggedRow" v-model:selection="selection" v-model:data="rules" :table-props="{sort:true, sortSelf: true, draggable: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<router-link class="button" to="/firewall/sourcenatrules/edit">Create</router-link>
|
||||
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/firewall/sourcenatrules/edit/' + selection[0]">Edit</router-link>
|
||||
<button @click="deleteRule" :disabled="selection.length != 1">Delete</button>
|
||||
</TableView>
|
||||
</div>
|
||||
</template>
|
|
@ -1,39 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../api";
|
||||
|
||||
async function doShit(){
|
||||
apiCall("Firewall.GetForwardRules", {});
|
||||
let links = $ref([]);
|
||||
let loading = $ref(false);
|
||||
|
||||
async function load(){
|
||||
loading = true
|
||||
let res = await apiCall("Network.GetLinks", {});
|
||||
if (res.Error === null) {
|
||||
console.debug("links", res.Data.Links);
|
||||
links = res.Data.Links;
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
let name = $ref("");
|
||||
let comment = $ref("");
|
||||
let counter = $ref(false);
|
||||
let options = $ref([{name: 'Accept'}, {name: 'Drop'}, {name: 'Continue'}]);
|
||||
onMounted(async() => {
|
||||
load();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="overflow-y: auto;">
|
||||
<PageHeader title="Dashboard">
|
||||
<button @click="doShit">Example Buttons</button>
|
||||
</PageHeader>
|
||||
<form @submit="$event => $event.preventDefault()" class="cl-secondary">
|
||||
<h3>Create Rule</h3>
|
||||
<label for="name" v-text="'Name'"/>
|
||||
<input name="name" v-model="name"/>
|
||||
<label for="counter" v-text="'Counter'"/>
|
||||
<input name="counter" type="checkbox" v-model="counter"/>
|
||||
<label for="comment" v-text="'Comment'"/>
|
||||
<textarea name="comment" v-model="comment"></textarea>
|
||||
<label for="verdict" v-text="'Verdict'"/>
|
||||
<pillbar :options="options" name="verdict" ></pillbar>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
<Multiselect/>
|
||||
asd
|
||||
<div v-if="!loading" v-for="(link, index) in links" :key="index">
|
||||
<p>{{ link.name }} {{ link.carrier_state }} {{ link.operational_state }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
66
client/src/pages/network/Interfaces.vue
Normal file
66
client/src/pages/network/Interfaces.vue
Normal file
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
|
||||
let interfaces = $ref({});
|
||||
let loading = $ref(false);
|
||||
let selection = $ref([] as number[]);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Name', path: 'name'},
|
||||
{heading: 'Type', path: 'type'},
|
||||
{heading: 'Members', path: 'members'},
|
||||
{heading: 'Addressing Mode', path: 'addressing_mode'},
|
||||
{heading: 'Address', path: 'address'},
|
||||
];
|
||||
|
||||
const displayData = $computed(() => {
|
||||
let data: any;
|
||||
data = [];
|
||||
for (const name in interfaces) {
|
||||
data.push({
|
||||
name,
|
||||
type: interfaces[name].type,
|
||||
addressing_mode: interfaces[name].addressing_mode,
|
||||
address: interfaces[name].address,
|
||||
comment: interfaces[name].comment,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
async function load(){
|
||||
loading = true
|
||||
let res = await apiCall("Network.GetInterfaces", {});
|
||||
if (res.Error === null) {
|
||||
console.debug("interfaces", res.Data.Interfaces);
|
||||
interfaces = res.Data.Interfaces;
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function deleteInterface(){
|
||||
let res = await apiCall("Network.DeleteInterface", {name: displayData[selection[0]].name});
|
||||
if (res.Error === null) {
|
||||
console.debug("deleted interface");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
load();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableView title="Interfaces" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<router-link class="button" to="/network/interfaces/edit">Create</router-link>
|
||||
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/network/interfaces/edit/' + selection[0]">Edit</router-link>
|
||||
<button @click="deleteInterface" :disabled="selection.length != 1">Delete</button>
|
||||
</TableView>
|
||||
</template>
|
51
client/src/pages/network/StaticRoutes.vue
Normal file
51
client/src/pages/network/StaticRoutes.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
|
||||
let staticRoutes = $ref([]);
|
||||
let loading = $ref(false);
|
||||
let selection = $ref([] as number[]);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Name', path: 'name'},
|
||||
{heading: 'Interface', path: 'interface'},
|
||||
{heading: 'Gateway', path: 'gateway'},
|
||||
{heading: 'Destination', path: 'destination'},
|
||||
{heading: 'Metric', path: 'metric'},
|
||||
];
|
||||
|
||||
async function load(){
|
||||
loading = true
|
||||
let res = await apiCall("Network.GetStaticRoutes", {});
|
||||
if (res.Error === null) {
|
||||
console.debug("staticRoutes", res.Data.StaticRoutes);
|
||||
staticRoutes = res.Data.StaticRoutes;
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function deleteStaticRoutes(){
|
||||
let res = await apiCall("Network.DeleteStaticRoute", {index: selection[0]});
|
||||
if (res.Error === null) {
|
||||
console.debug("deleted static routes");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
load();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableView title="Static Routes" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="staticRoutes" :table-props="{sort:true, sortSelf: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<router-link class="button" to="/network/staticroutes/edit">Create</router-link>
|
||||
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/network/staticroutes/edit/' + selection[0]">Edit</router-link>
|
||||
<button @click="deleteStaticRoutes" :disabled="selection.length != 1">Delete</button>
|
||||
</TableView>
|
||||
</template>
|
|
@ -2,6 +2,9 @@
|
|||
import { apiCall } from "../../api";
|
||||
|
||||
let addresses = $ref([]);
|
||||
let loading = $ref(false);
|
||||
let selection = $ref([] as number[]);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Name', path: 'name'},
|
||||
{heading: 'Type', path: 'type'},
|
||||
|
@ -10,6 +13,7 @@ const columns = [
|
|||
];
|
||||
|
||||
async function load(){
|
||||
loading = true
|
||||
let res = await apiCall("Object.GetAddresses", {});
|
||||
if (res.Error === null) {
|
||||
addresses = res.Data.Addresses;
|
||||
|
@ -17,6 +21,52 @@ async function load(){
|
|||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
const displayData = $computed(() => {
|
||||
let data: any;
|
||||
data = [];
|
||||
for (const name in addresses) {
|
||||
data.push({
|
||||
name,
|
||||
value: getAddressValue(addresses[name]),
|
||||
type: addresses[name].type,
|
||||
comment: addresses[name].comment,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
function getAddressValue(s: any): string {
|
||||
let value: string;
|
||||
switch (s.type) {
|
||||
case "host":
|
||||
value = s.host;
|
||||
break;
|
||||
case "range":
|
||||
value = s.range;
|
||||
break;
|
||||
case "network":
|
||||
value = s.network;
|
||||
break;
|
||||
case "group":
|
||||
value = s.children;
|
||||
break;
|
||||
default:
|
||||
value = "unkown";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function deleteAddress(){
|
||||
let res = await apiCall("Object.DeleteAddress", {name: displayData[selection[0]].name});
|
||||
if (res.Error === null) {
|
||||
console.debug("deleted address");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
|
@ -26,10 +76,10 @@ onMounted(async() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="Addresses">
|
||||
<button @click="load">Load Addresses</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="columns" v-model:data="addresses"/>
|
||||
</div>
|
||||
<TableView title="Addresses" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<router-link class="button" to="/object/addresses/edit">Create</router-link>
|
||||
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/object/addresses/edit/' + selection[0]">Edit</router-link>
|
||||
<button @click="deleteAddress" :disabled="selection.length != 1">Delete</button>
|
||||
</TableView>
|
||||
</template>
|
|
@ -1,7 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { apiCall } from "../../api";
|
||||
|
||||
let services = $ref([]);
|
||||
let services = $ref({});
|
||||
let loading = $ref(false);
|
||||
let selection = $ref([] as number[]);
|
||||
|
||||
const columns = [
|
||||
{heading: 'Name', path: 'name'},
|
||||
{heading: 'Type', path: 'type'},
|
||||
|
@ -9,14 +12,66 @@ const columns = [
|
|||
{heading: 'Comment', path: 'comment'},
|
||||
];
|
||||
|
||||
const displayData = $computed(() => {
|
||||
let data: any;
|
||||
data = [];
|
||||
for (const name in services) {
|
||||
data.push({
|
||||
name,
|
||||
value: getServiceValue(services[name]),
|
||||
type: services[name].type,
|
||||
comment: services[name].comment,
|
||||
});
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
function getServiceValue(s: any): string {
|
||||
let value: string;
|
||||
switch (s.type) {
|
||||
case "tcp":
|
||||
case "udp":
|
||||
value = getServicePortRange(s);
|
||||
break;
|
||||
case "icmp":
|
||||
value = "icmp";
|
||||
break;
|
||||
case "group":
|
||||
value = s.children;
|
||||
break;
|
||||
default:
|
||||
value = "unkown";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getServicePortRange(s:any): string {
|
||||
if (s.dport_end) {
|
||||
return s.dport_start + "-" + s.dport_end;
|
||||
}
|
||||
return s.dport_start;
|
||||
}
|
||||
|
||||
async function load(){
|
||||
loading = true
|
||||
let res = await apiCall("Object.GetServices", {});
|
||||
if (res.Error === null) {
|
||||
console.debug("services", res.Data.Services);
|
||||
services = res.Data.Services;
|
||||
console.debug("services", services);
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function deleteService(){
|
||||
let res = await apiCall("Object.DeleteService", {name: displayData[selection[0]].name});
|
||||
if (res.Error === null) {
|
||||
console.debug("deleted service");
|
||||
} else {
|
||||
console.debug("error", res);
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
onMounted(async() => {
|
||||
|
@ -26,10 +81,10 @@ onMounted(async() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="services">
|
||||
<button @click="load">Load Services</button>
|
||||
</PageHeader>
|
||||
<NiceTable :columns="columns" v-model:data="services"/>
|
||||
</div>
|
||||
<TableView title="Services" :columns="columns" :loading="loading" v-model:selection="selection" v-model:data="displayData" :table-props="{sort:true, sortSelf: true}">
|
||||
<button @click="load">Refresh</button>
|
||||
<router-link class="button" to="/object/services/edit">Create</router-link>
|
||||
<router-link class="button" :class="{ disabled: selection.length != 1 }" :to="'/object/services/edit/' + selection[0]">Edit</router-link>
|
||||
<button @click="deleteService" :disabled="selection.length != 1">Delete</button>
|
||||
</TableView>
|
||||
</template>
|
9
client/src/plugins.ts
Normal file
9
client/src/plugins.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from "vue-toast-notification";
|
||||
|
||||
export default function initiateCommonPlugins() {
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
return { router, toast };
|
||||
}
|
4
go.mod
4
go.mod
|
@ -13,8 +13,12 @@ require (
|
|||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/klauspost/compress v1.10.3 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/r3labs/diff/v3 v3.0.1 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/crypto v0.5.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -23,6 +23,8 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
|||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
|
@ -48,6 +50,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
|||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg=
|
||||
github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
|
@ -56,6 +60,10 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
|||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s=
|
||||
go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
|
|
7
internal/api/config/config.go
Normal file
7
internal/api/config/config.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package config
|
||||
|
||||
import "nfsense.net/nfsense/internal/config"
|
||||
|
||||
type Config struct {
|
||||
ConfigManager *config.ConfigManager
|
||||
}
|
40
internal/api/config/pending.go
Normal file
40
internal/api/config/pending.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/r3labs/diff/v3"
|
||||
)
|
||||
|
||||
type GetPendingStatusResult struct {
|
||||
Changed bool
|
||||
}
|
||||
|
||||
func (c *Config) GetPendingStatus(ctx context.Context, params struct{}) (GetPendingStatusResult, error) {
|
||||
return GetPendingStatusResult{
|
||||
Changed: c.ConfigManager.AreChangesPending(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GetPendingChangelogResult struct {
|
||||
Changelog diff.Changelog
|
||||
}
|
||||
|
||||
func (c *Config) GetPendingChangelog(ctx context.Context, params struct{}) (GetPendingChangelogResult, error) {
|
||||
log, err := c.ConfigManager.GetPendingChangelog()
|
||||
if err != nil {
|
||||
return GetPendingChangelogResult{}, fmt.Errorf("Get Pending changelog %w", err)
|
||||
}
|
||||
return GetPendingChangelogResult{
|
||||
Changelog: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Config) ApplyPendingChanges(ctx context.Context, params struct{}) (struct{}, error) {
|
||||
return struct{}{}, c.ConfigManager.ApplyPendingChanges()
|
||||
}
|
||||
|
||||
func (c *Config) DiscardPendingChanges(ctx context.Context, params struct{}) (struct{}, error) {
|
||||
return struct{}{}, c.ConfigManager.DiscardPendingConfig()
|
||||
}
|
|
@ -2,19 +2,85 @@ package firewall
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type GetDestinationNATRulesParameters struct {
|
||||
}
|
||||
|
||||
type GetDestinationNATRulesResult struct {
|
||||
DestinationNATRules []definitions.DestinationNATRule
|
||||
DestinationNATRules []definitions.DestinationNATRule `json:"destination_nat_rules"`
|
||||
}
|
||||
|
||||
func (f *Firewall) GetDestinationNATRules(ctx context.Context, params GetForwardRulesParameters) (GetDestinationNATRulesResult, error) {
|
||||
func (f *Firewall) GetDestinationNATRules(ctx context.Context, params struct{}) (GetDestinationNATRulesResult, error) {
|
||||
return GetDestinationNATRulesResult{
|
||||
DestinationNATRules: f.Conf.Firewall.DestinationNATRules,
|
||||
DestinationNATRules: f.ConfigManager.GetPendingConfig().Firewall.DestinationNATRules,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateDestinationNATRuleParameters struct {
|
||||
DestinationNATRule definitions.DestinationNATRule `json:"destination_nat_rule"`
|
||||
}
|
||||
|
||||
func (f *Firewall) CreateDestinationNATRule(ctx context.Context, params CreateDestinationNATRuleParameters) (struct{}, error) {
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.DestinationNATRules = append(conf.Firewall.DestinationNATRules, params.DestinationNATRule)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type UpdateDestinationNATRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
DestinationNATRule definitions.DestinationNATRule `json:"destination_nat_rule"`
|
||||
}
|
||||
|
||||
func (f *Firewall) UpdateDestinationNATRule(ctx context.Context, params UpdateDestinationNATRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.DestinationNATRules) {
|
||||
return struct{}{}, fmt.Errorf("DestinationNATRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.DestinationNATRules[params.Index] = params.DestinationNATRule
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type MoveDestinationNATRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
ToIndex uint64 `json:"to_index"`
|
||||
}
|
||||
|
||||
func (f *Firewall) MoveDestinationNATRule(ctx context.Context, params MoveDestinationNATRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.DestinationNATRules) {
|
||||
return struct{}{}, fmt.Errorf("DestinationNATRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
rule := conf.Firewall.DestinationNATRules[params.Index]
|
||||
sliceWithoutRule := append(conf.Firewall.DestinationNATRules[:params.Index], conf.Firewall.DestinationNATRules[params.Index+1:]...)
|
||||
newSlice := make([]definitions.DestinationNATRule, params.ToIndex+1)
|
||||
copy(newSlice, sliceWithoutRule[:params.ToIndex])
|
||||
newSlice[params.ToIndex] = rule
|
||||
conf.Firewall.DestinationNATRules = append(newSlice, sliceWithoutRule[params.ToIndex:]...)
|
||||
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type DeleteDestinationNATRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
}
|
||||
|
||||
func (f *Firewall) DeleteDestinationNATRule(ctx context.Context, params DeleteDestinationNATRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.DestinationNATRules) {
|
||||
return struct{}{}, fmt.Errorf("DestinationNATRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.DestinationNATRules = append(conf.Firewall.DestinationNATRules[:params.Index], conf.Firewall.DestinationNATRules[params.Index+1:]...)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package firewall
|
||||
|
||||
import (
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
"nfsense.net/nfsense/internal/config"
|
||||
)
|
||||
|
||||
type Firewall struct {
|
||||
Conf *definitions.Config
|
||||
ConfigManager *config.ConfigManager
|
||||
}
|
||||
|
|
|
@ -2,19 +2,85 @@ package firewall
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type GetForwardRulesParameters struct {
|
||||
}
|
||||
|
||||
type GetForwardRulesResult struct {
|
||||
ForwardRules []definitions.ForwardRule
|
||||
ForwardRules []definitions.ForwardRule `json:"forward_rules"`
|
||||
}
|
||||
|
||||
func (f *Firewall) GetForwardRules(ctx context.Context, params GetForwardRulesParameters) (GetForwardRulesResult, error) {
|
||||
func (f *Firewall) GetForwardRules(ctx context.Context, params struct{}) (GetForwardRulesResult, error) {
|
||||
return GetForwardRulesResult{
|
||||
ForwardRules: f.Conf.Firewall.ForwardRules,
|
||||
ForwardRules: f.ConfigManager.GetPendingConfig().Firewall.ForwardRules,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateForwardRuleParameters struct {
|
||||
ForwardRule definitions.ForwardRule `json:"forward_rule"`
|
||||
}
|
||||
|
||||
func (f *Firewall) CreateForwardRule(ctx context.Context, params CreateForwardRuleParameters) (struct{}, error) {
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.ForwardRules = append(conf.Firewall.ForwardRules, params.ForwardRule)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type UpdateForwardRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
ForwardRule definitions.ForwardRule `json:"forward_rule"`
|
||||
}
|
||||
|
||||
func (f *Firewall) UpdateForwardRule(ctx context.Context, params UpdateForwardRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.ForwardRules) {
|
||||
return struct{}{}, fmt.Errorf("ForwardRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.ForwardRules[params.Index] = params.ForwardRule
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type MoveForwardRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
ToIndex uint64 `json:"to_index"`
|
||||
}
|
||||
|
||||
func (f *Firewall) MoveForwardRule(ctx context.Context, params MoveForwardRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.ForwardRules) {
|
||||
return struct{}{}, fmt.Errorf("ForwardRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
rule := conf.Firewall.ForwardRules[params.Index]
|
||||
sliceWithoutRule := append(conf.Firewall.ForwardRules[:params.Index], conf.Firewall.ForwardRules[params.Index+1:]...)
|
||||
newSlice := make([]definitions.ForwardRule, params.ToIndex+1)
|
||||
copy(newSlice, sliceWithoutRule[:params.ToIndex])
|
||||
newSlice[params.ToIndex] = rule
|
||||
conf.Firewall.ForwardRules = append(newSlice, sliceWithoutRule[params.ToIndex:]...)
|
||||
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type DeleteForwardRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
}
|
||||
|
||||
func (f *Firewall) DeleteForwardRule(ctx context.Context, params DeleteForwardRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.ForwardRules) {
|
||||
return struct{}{}, fmt.Errorf("ForwardRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.ForwardRules = append(conf.Firewall.ForwardRules[:params.Index], conf.Firewall.ForwardRules[params.Index+1:]...)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
|
|
@ -2,19 +2,85 @@ package firewall
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type GetSourceNATRulesParameters struct {
|
||||
}
|
||||
|
||||
type GetSourceNATRulesResult struct {
|
||||
SourceNATRules []definitions.SourceNATRule
|
||||
SourceNATRules []definitions.SourceNATRule `json:"source_nat_rules"`
|
||||
}
|
||||
|
||||
func (f *Firewall) GetSourceNATRules(ctx context.Context, params GetForwardRulesParameters) (GetSourceNATRulesResult, error) {
|
||||
func (f *Firewall) GetSourceNATRules(ctx context.Context, params struct{}) (GetSourceNATRulesResult, error) {
|
||||
return GetSourceNATRulesResult{
|
||||
SourceNATRules: f.Conf.Firewall.SourceNATRules,
|
||||
SourceNATRules: f.ConfigManager.GetPendingConfig().Firewall.SourceNATRules,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateSourceNATRuleParameters struct {
|
||||
SourceNATRule definitions.SourceNATRule `json:"source_nat_rule"`
|
||||
}
|
||||
|
||||
func (f *Firewall) CreateSourceNATRule(ctx context.Context, params CreateSourceNATRuleParameters) (struct{}, error) {
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.SourceNATRules = append(conf.Firewall.SourceNATRules, params.SourceNATRule)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type UpdateSourceNATRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
SourceNATRule definitions.SourceNATRule `json:"source_nat_rule"`
|
||||
}
|
||||
|
||||
func (f *Firewall) UpdateSourceNATRule(ctx context.Context, params UpdateSourceNATRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.SourceNATRules) {
|
||||
return struct{}{}, fmt.Errorf("SourceNATRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.SourceNATRules[params.Index] = params.SourceNATRule
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type MoveSourceNATRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
ToIndex uint64 `json:"to_index"`
|
||||
}
|
||||
|
||||
func (f *Firewall) MoveSourceNATRule(ctx context.Context, params MoveSourceNATRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.SourceNATRules) {
|
||||
return struct{}{}, fmt.Errorf("SourceNATRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
rule := conf.Firewall.SourceNATRules[params.Index]
|
||||
sliceWithoutRule := append(conf.Firewall.SourceNATRules[:params.Index], conf.Firewall.SourceNATRules[params.Index+1:]...)
|
||||
newSlice := make([]definitions.SourceNATRule, params.ToIndex+1)
|
||||
copy(newSlice, sliceWithoutRule[:params.ToIndex])
|
||||
newSlice[params.ToIndex] = rule
|
||||
conf.Firewall.SourceNATRules = append(newSlice, sliceWithoutRule[params.ToIndex:]...)
|
||||
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type DeleteSourceNATRuleParameters struct {
|
||||
Index uint64 `json:"index"`
|
||||
}
|
||||
|
||||
func (f *Firewall) DeleteSourceNATRule(ctx context.Context, params DeleteSourceNATRuleParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.SourceNATRules) {
|
||||
return struct{}{}, fmt.Errorf("SourceNATRule does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Firewall.SourceNATRules = append(conf.Firewall.SourceNATRules[:params.Index], conf.Firewall.SourceNATRules[params.Index+1:]...)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
|
86
internal/api/network/interfaces.go
Normal file
86
internal/api/network/interfaces.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
"nfsense.net/nfsense/internal/networkd/dbus"
|
||||
)
|
||||
|
||||
type GetLinksResult struct {
|
||||
Links []dbus.Link
|
||||
}
|
||||
|
||||
func (f *Network) GetLinks(ctx context.Context, params struct{}) (GetLinksResult, error) {
|
||||
links, err := dbus.GetLinks(*f.DbusConn)
|
||||
if err != nil {
|
||||
return GetLinksResult{}, fmt.Errorf("Getting Links: %w", err)
|
||||
}
|
||||
return GetLinksResult{
|
||||
Links: links,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type GetInterfacesResult struct {
|
||||
Interfaces map[string]definitions.Interface
|
||||
}
|
||||
|
||||
func (f *Network) GetInterfaces(ctx context.Context, params struct{}) (GetInterfacesResult, error) {
|
||||
return GetInterfacesResult{
|
||||
Interfaces: f.ConfigManager.GetPendingConfig().Network.Interfaces,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateInterfaceParameters struct {
|
||||
Name string
|
||||
definitions.Interface
|
||||
}
|
||||
|
||||
func (f *Network) CreateInterface(ctx context.Context, params CreateInterfaceParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name]
|
||||
if ok {
|
||||
return struct{}{}, fmt.Errorf("Interface already Exists")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Network.Interfaces[params.Name] = params.Interface
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type UpdateInterfaceParameters struct {
|
||||
Name string
|
||||
definitions.Interface
|
||||
}
|
||||
|
||||
func (f *Network) UpdateInterface(ctx context.Context, params UpdateInterfaceParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name]
|
||||
if !ok {
|
||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Network.Interfaces[params.Name] = params.Interface
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type DeleteInterfaceParameters struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (f *Network) DeleteInterface(ctx context.Context, params DeleteInterfaceParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name]
|
||||
if !ok {
|
||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
delete(conf.Network.Interfaces, params.Name)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
11
internal/api/network/network.go
Normal file
11
internal/api/network/network.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"github.com/godbus/dbus/v5"
|
||||
"nfsense.net/nfsense/internal/config"
|
||||
)
|
||||
|
||||
type Network struct {
|
||||
ConfigManager *config.ConfigManager
|
||||
DbusConn *dbus.Conn
|
||||
}
|
59
internal/api/network/static_routes.go
Normal file
59
internal/api/network/static_routes.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type GetStaticRoutesResult struct {
|
||||
StaticRoutes []definitions.StaticRoute
|
||||
}
|
||||
|
||||
func (f *Network) GetStaticRoutes(ctx context.Context, params struct{}) (GetStaticRoutesResult, error) {
|
||||
return GetStaticRoutesResult{
|
||||
StaticRoutes: f.ConfigManager.GetPendingConfig().Network.StaticRoutes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *Network) CreateStaticRoute(ctx context.Context, params definitions.StaticRoute) (struct{}, error) {
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Network.StaticRoutes = append(conf.Network.StaticRoutes, params)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type UpdateStaticRouteParameters struct {
|
||||
Index uint
|
||||
definitions.StaticRoute
|
||||
}
|
||||
|
||||
func (f *Network) UpdateStaticRoute(ctx context.Context, params UpdateStaticRouteParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.DestinationNATRules) {
|
||||
return struct{}{}, fmt.Errorf("StaticRoute does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Network.StaticRoutes = append(conf.Network.StaticRoutes, params.StaticRoute)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type DeleteStaticRouteParameters struct {
|
||||
Index uint
|
||||
}
|
||||
|
||||
func (f *Network) DeleteStaticRoute(ctx context.Context, params DeleteStaticRouteParameters) (struct{}, error) {
|
||||
if int(params.Index) >= len(f.ConfigManager.GetPendingConfig().Firewall.DestinationNATRules) {
|
||||
return struct{}{}, fmt.Errorf("StaticRoute does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Network.StaticRoutes = append(conf.Network.StaticRoutes[:params.Index], conf.Network.StaticRoutes[params.Index+1:]...)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
|
@ -2,19 +2,70 @@ package object
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type GetAddressesParameters struct {
|
||||
}
|
||||
|
||||
type GetAddressesResult struct {
|
||||
Addresses map[string]definitions.Address
|
||||
}
|
||||
|
||||
func (f *Object) GetAddresses(ctx context.Context, params GetAddressesParameters) (GetAddressesResult, error) {
|
||||
func (f *Object) GetAddresses(ctx context.Context, params struct{}) (GetAddressesResult, error) {
|
||||
return GetAddressesResult{
|
||||
Addresses: f.Conf.Object.Addresses,
|
||||
Addresses: f.ConfigManager.GetPendingConfig().Object.Addresses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateAddressParameters struct {
|
||||
Name string
|
||||
Address definitions.Address
|
||||
}
|
||||
|
||||
func (f *Object) CreateAddress(ctx context.Context, params CreateAddressParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name]
|
||||
if ok {
|
||||
return struct{}{}, fmt.Errorf("Address already Exists")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Object.Addresses[params.Name] = params.Address
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type UpdateAddressParameters struct {
|
||||
Name string
|
||||
Address definitions.Address
|
||||
}
|
||||
|
||||
func (f *Object) UpdateAddress(ctx context.Context, params UpdateAddressParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name]
|
||||
if !ok {
|
||||
return struct{}{}, fmt.Errorf("Address does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Object.Addresses[params.Name] = params.Address
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type DeleteAddressParameters struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (f *Object) DeleteAddress(ctx context.Context, params DeleteAddressParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name]
|
||||
if !ok {
|
||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
delete(conf.Object.Addresses, params.Name)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package object
|
||||
|
||||
import "nfsense.net/nfsense/internal/definitions"
|
||||
import "nfsense.net/nfsense/internal/config"
|
||||
|
||||
type Object struct {
|
||||
Conf *definitions.Config
|
||||
ConfigManager *config.ConfigManager
|
||||
}
|
||||
|
|
|
@ -2,19 +2,70 @@ package object
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type GetServicesParameters struct {
|
||||
}
|
||||
|
||||
type GetServicesResult struct {
|
||||
Services map[string]definitions.Service
|
||||
}
|
||||
|
||||
func (f *Object) GetServices(ctx context.Context, params GetServicesParameters) (GetServicesResult, error) {
|
||||
func (f *Object) GetServices(ctx context.Context, params struct{}) (GetServicesResult, error) {
|
||||
return GetServicesResult{
|
||||
Services: f.Conf.Object.Services,
|
||||
Services: f.ConfigManager.GetPendingConfig().Object.Services,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CreateServiceParameters struct {
|
||||
Name string
|
||||
Service definitions.Service
|
||||
}
|
||||
|
||||
func (f *Object) CreateService(ctx context.Context, params CreateServiceParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Object.Services[params.Name]
|
||||
if ok {
|
||||
return struct{}{}, fmt.Errorf("Service already Exists")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Object.Services[params.Name] = params.Service
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type UpdateServiceParameters struct {
|
||||
Name string
|
||||
Service definitions.Service
|
||||
}
|
||||
|
||||
func (f *Object) UpdateService(ctx context.Context, params UpdateServiceParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Object.Services[params.Name]
|
||||
if !ok {
|
||||
return struct{}{}, fmt.Errorf("Service does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
conf.Object.Services[params.Name] = params.Service
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
||||
type DeleteServiceParameters struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (f *Object) DeleteService(ctx context.Context, params DeleteServiceParameters) (struct{}, error) {
|
||||
_, ok := f.ConfigManager.GetPendingConfig().Object.Services[params.Name]
|
||||
if !ok {
|
||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||
}
|
||||
|
||||
t, conf := f.ConfigManager.StartTransaction()
|
||||
defer t.Discard()
|
||||
|
||||
delete(conf.Object.Services, params.Name)
|
||||
return struct{}{}, t.Commit()
|
||||
}
|
||||
|
|
52
internal/config/apply.go
Normal file
52
internal/config/apply.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
// ApplyPendingChanges Takes all pending Changes and Tries to Apply them using the Registered Apply Functions.
|
||||
// In Case of error it Attempts to Revert to the Current Config
|
||||
func (m *ConfigManager) ApplyPendingChanges() error {
|
||||
slog.Info("Applying Pending Changes...")
|
||||
for _, fn := range m.applyFunctions {
|
||||
err := fn(*m.currentConfig, *m.pendingConfig)
|
||||
if err != nil {
|
||||
slog.Error("Applying Pending Changes", err)
|
||||
err2 := revertToCurrent(m)
|
||||
if err2 != nil {
|
||||
slog.Error("Reverting Error", err2)
|
||||
return fmt.Errorf("Apply Error %w; Reverting Error %w", err, err2)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.currentConfig = m.pendingConfig.Clone()
|
||||
|
||||
err := m.saveConfig(m.currentConfigFilePath, m.pendingConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Save Current Config: %w", err)
|
||||
}
|
||||
err = os.Remove(m.pendingConfigFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Delete Pending Config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func revertToCurrent(m *ConfigManager) error {
|
||||
for _, fn := range m.applyFunctions {
|
||||
err := fn(*m.pendingConfig, *m.currentConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ConfigManager) RegisterApplyFunction(fn func(currentConfig definitions.Config, pendingConfig definitions.Config) error) {
|
||||
m.applyFunctions = append(m.applyFunctions, fn)
|
||||
}
|
11
internal/config/diff.go
Normal file
11
internal/config/diff.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package config
|
||||
|
||||
import "github.com/r3labs/diff/v3"
|
||||
|
||||
func (m *ConfigManager) AreChangesPending() bool {
|
||||
return diff.Changed(m.currentConfig, m.pendingConfig)
|
||||
}
|
||||
|
||||
func (m *ConfigManager) GetPendingChangelog() (diff.Changelog, error) {
|
||||
return diff.Diff(m.currentConfig, m.pendingConfig, diff.SliceOrdering(true))
|
||||
}
|
16
internal/config/discard.go
Normal file
16
internal/config/discard.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (m *ConfigManager) DiscardPendingConfig() error {
|
||||
m.pendingConfig = m.currentConfig.Clone()
|
||||
|
||||
err := os.Remove(m.pendingConfigFilePath)
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
11
internal/config/get.go
Normal file
11
internal/config/get.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package config
|
||||
|
||||
import "nfsense.net/nfsense/internal/definitions"
|
||||
|
||||
func (m *ConfigManager) GetCurrentConfig() definitions.Config {
|
||||
return *m.currentConfig.Clone()
|
||||
}
|
||||
|
||||
func (m *ConfigManager) GetPendingConfig() definitions.Config {
|
||||
return *m.pendingConfig.Clone()
|
||||
}
|
57
internal/config/load.go
Normal file
57
internal/config/load.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
func (m *ConfigManager) LoadCurrentConfigFromDisk() error {
|
||||
var config definitions.Config
|
||||
configFile, err := os.Open(m.currentConfigFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening Config File %w", err)
|
||||
}
|
||||
defer configFile.Close()
|
||||
|
||||
jsonParser := json.NewDecoder(configFile)
|
||||
jsonParser.DisallowUnknownFields()
|
||||
err = jsonParser.Decode(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding Config File %w", err)
|
||||
}
|
||||
|
||||
err = definitions.ValidateConfig(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating Config: %w", err)
|
||||
}
|
||||
|
||||
m.currentConfig = &config
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ConfigManager) LoadPendingConfigFromDisk() error {
|
||||
var config definitions.Config
|
||||
configFile, err := os.Open(m.pendingConfigFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening Config File %w", err)
|
||||
}
|
||||
defer configFile.Close()
|
||||
|
||||
jsonParser := json.NewDecoder(configFile)
|
||||
jsonParser.DisallowUnknownFields()
|
||||
err = jsonParser.Decode(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding Config File %w", err)
|
||||
}
|
||||
|
||||
err = definitions.ValidateConfig(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating Config: %w", err)
|
||||
}
|
||||
|
||||
m.pendingConfig = &config
|
||||
return nil
|
||||
}
|
29
internal/config/manager.go
Normal file
29
internal/config/manager.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type ConfigManager struct {
|
||||
currentConfigFilePath string
|
||||
pendingConfigFilePath string
|
||||
|
||||
currentConfig *definitions.Config
|
||||
pendingConfig *definitions.Config
|
||||
|
||||
transactionMutex sync.Mutex
|
||||
|
||||
applyFunctions []func(currentConfig definitions.Config, pendingConfig definitions.Config) error
|
||||
}
|
||||
|
||||
func CreateConfigManager() *ConfigManager {
|
||||
manager := ConfigManager{
|
||||
currentConfigFilePath: "config.json",
|
||||
pendingConfigFilePath: "pending.json",
|
||||
currentConfig: &definitions.Config{},
|
||||
pendingConfig: &definitions.Config{},
|
||||
}
|
||||
return &manager
|
||||
}
|
23
internal/config/save.go
Normal file
23
internal/config/save.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
func (m *ConfigManager) saveConfig(path string, conf *definitions.Config) error {
|
||||
data, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Marshal Config: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(path, data, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Write Config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
62
internal/config/transaction.go
Normal file
62
internal/config/transaction.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type ConfigTransaction struct {
|
||||
finished bool
|
||||
mutex sync.Mutex
|
||||
configManager *ConfigManager
|
||||
changes *definitions.Config
|
||||
}
|
||||
|
||||
func (m *ConfigManager) StartTransaction() (*ConfigTransaction, *definitions.Config) {
|
||||
m.transactionMutex.Lock()
|
||||
confCopy := m.pendingConfig.Clone()
|
||||
return &ConfigTransaction{
|
||||
configManager: m,
|
||||
changes: confCopy,
|
||||
}, confCopy
|
||||
}
|
||||
|
||||
func (t *ConfigTransaction) Commit() error {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
if t.finished {
|
||||
return fmt.Errorf("transaction already finished")
|
||||
}
|
||||
|
||||
t.finished = true
|
||||
defer t.configManager.transactionMutex.Unlock()
|
||||
|
||||
err := definitions.ValidateConfig(t.changes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validating Config before Apply: %w", err)
|
||||
}
|
||||
|
||||
err = t.configManager.saveConfig(t.configManager.pendingConfigFilePath, t.changes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Save Current Config: %w", err)
|
||||
}
|
||||
|
||||
t.configManager.pendingConfig = t.changes.Clone()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Discard Discards the Transaction.
|
||||
// Is a noop if The Transaction Already Finished due to a Commit() or another Discard()
|
||||
func (t *ConfigTransaction) Discard() {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
|
||||
if !t.finished {
|
||||
t.finished = true
|
||||
t.configManager.transactionMutex.Unlock()
|
||||
}
|
||||
}
|
|
@ -8,12 +8,12 @@ import (
|
|||
)
|
||||
|
||||
type Address struct {
|
||||
Type AddressType `json:"type" validate:"min=0,max=3"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Host *netip.Addr `json:"host,omitempty" validate:"excluded_unless=Type 0"`
|
||||
Range *netipx.IPRange `json:"range,omitempty" validate:"excluded_unless=Type 1"`
|
||||
Network *IPNet `json:"network,omitempty" validate:"excluded_unless=Type 2"`
|
||||
Children *[]string `json:"children,omitempty"`
|
||||
Type AddressType `json:"type" validate:"min=0,max=3"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Host *netip.Addr `json:"host,omitempty" validate:"excluded_unless=Type 0"`
|
||||
Range *netipx.IPRange `json:"range,omitempty" validate:"excluded_unless=Type 1"`
|
||||
NetworkAddress *IPNet `json:"network,omitempty" validate:"excluded_unless=Type 2"`
|
||||
Children *[]string `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
type AddressType int
|
||||
|
@ -21,7 +21,7 @@ type AddressType int
|
|||
const (
|
||||
Host AddressType = iota
|
||||
Range
|
||||
Network
|
||||
NetworkAddress
|
||||
AddressGroup
|
||||
)
|
||||
|
||||
|
@ -33,7 +33,7 @@ func (t *AddressType) FromString(input string) AddressType {
|
|||
return map[string]AddressType{
|
||||
"host": Host,
|
||||
"range": Range,
|
||||
"network": Network,
|
||||
"network": NetworkAddress,
|
||||
"group": AddressGroup,
|
||||
}[input]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package definitions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
@ -11,6 +12,21 @@ type Config struct {
|
|||
ConfigVersion uint64 `json:"config_version" validate:"required,eq=1"`
|
||||
Firewall Firewall `json:"firewall" validate:"required,dive"`
|
||||
Object Object `json:"object" validate:"required,dive"`
|
||||
Network Network `json:"network" validate:"required,dive"`
|
||||
}
|
||||
|
||||
// Clone TODO find a better way to deep copy
|
||||
func (c *Config) Clone() *Config {
|
||||
data, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Marshal Error: %w", err))
|
||||
}
|
||||
var clone Config
|
||||
err = json.Unmarshal(data, &clone)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Unmarshal Error: %w", err))
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
func ValidateConfig(conf *Config) error {
|
||||
|
|
30
internal/definitions/hardwareaddress.go
Normal file
30
internal/definitions/hardwareaddress.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package definitions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
)
|
||||
|
||||
type HardwareAddress struct {
|
||||
net.HardwareAddr
|
||||
}
|
||||
|
||||
// MarshalJSON for IPCIDR
|
||||
func (i HardwareAddress) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON for IPCIDR
|
||||
func (i *HardwareAddress) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.HardwareAddr = mac
|
||||
return nil
|
||||
}
|
89
internal/definitions/interface.go
Normal file
89
internal/definitions/interface.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package definitions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Interface struct {
|
||||
Alias string `json:"alias,omitempty" validate:"min=0,max=3"`
|
||||
Type InterfaceType `json:"type" validate:"min=0,max=3"`
|
||||
AddressingMode InterfaceAddressingMode `json:"addressing_mode" validate:"min=0,max=2"`
|
||||
Address *IPCIDR `json:"address,omitempty" validate:"excluded_unless=AddressingMode 1"`
|
||||
HardwareDevice *string `json:"hardware_device,omitempty"`
|
||||
// TODO fix Validator for int pointers with min=0,max=4094
|
||||
VlanID *uint `json:"vlan_id,omitempty"`
|
||||
VlanParent *string `json:"vlan_parent,omitempty"`
|
||||
BondMembers *[]string `json:"bond_members,omitempty"`
|
||||
BridgeMembers *[]string `json:"bridge_members,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
type InterfaceType int
|
||||
|
||||
const (
|
||||
Hardware InterfaceType = iota
|
||||
Vlan
|
||||
Bond
|
||||
Bridge
|
||||
)
|
||||
|
||||
func (t InterfaceType) String() string {
|
||||
return [...]string{"hardware", "vlan", "bond", "bridge"}[t]
|
||||
}
|
||||
|
||||
func (t *InterfaceType) FromString(input string) InterfaceType {
|
||||
return map[string]InterfaceType{
|
||||
"hardware": Hardware,
|
||||
"vlan": Vlan,
|
||||
"bond": Bond,
|
||||
"bridge": Bridge,
|
||||
}[input]
|
||||
}
|
||||
|
||||
func (t InterfaceType) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
func (t *InterfaceType) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
err := json.Unmarshal(b, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = t.FromString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
type InterfaceAddressingMode int
|
||||
|
||||
const (
|
||||
None InterfaceAddressingMode = iota
|
||||
Static
|
||||
Dhcp
|
||||
)
|
||||
|
||||
func (t InterfaceAddressingMode) String() string {
|
||||
return [...]string{"none", "static", "dhcp"}[t]
|
||||
}
|
||||
|
||||
func (t *InterfaceAddressingMode) FromString(input string) InterfaceAddressingMode {
|
||||
return map[string]InterfaceAddressingMode{
|
||||
"none": None,
|
||||
"static": Static,
|
||||
"dhcp": Dhcp,
|
||||
}[input]
|
||||
}
|
||||
|
||||
func (t InterfaceAddressingMode) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.String())
|
||||
}
|
||||
|
||||
func (t *InterfaceAddressingMode) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
err := json.Unmarshal(b, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = t.FromString(s)
|
||||
return nil
|
||||
}
|
32
internal/definitions/ipcidr.go
Normal file
32
internal/definitions/ipcidr.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package definitions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
)
|
||||
|
||||
// IPCIDR is IP Address with the mask in CIDR format
|
||||
type IPCIDR struct {
|
||||
net.IPNet
|
||||
}
|
||||
|
||||
// MarshalJSON for IPCIDR
|
||||
func (i IPCIDR) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(i.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON for IPCIDR
|
||||
func (i *IPCIDR) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ip, ipnet, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.IPNet = *ipnet
|
||||
i.IPNet.IP = ip
|
||||
return nil
|
||||
}
|
6
internal/definitions/network.go
Normal file
6
internal/definitions/network.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package definitions
|
||||
|
||||
type Network struct {
|
||||
Interfaces map[string]Interface `json:"interfaces" validate:"required,dive"`
|
||||
StaticRoutes []StaticRoute `json:"static_routes" validate:"required,dive"`
|
||||
}
|
13
internal/definitions/static_route.go
Normal file
13
internal/definitions/static_route.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package definitions
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type StaticRoute struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Interface string `json:"interface,omitempty"`
|
||||
Gateway netip.Addr `json:"gateway,omitempty"`
|
||||
Destination IPNet `json:"destination,omitempty"`
|
||||
Metric uint `json:"metric,omitempty"`
|
||||
}
|
|
@ -81,7 +81,8 @@ func (h *Handler) HandleRequest(ctx context.Context, s *session.Session, r io.Re
|
|||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
slog.Error("Recovered Panic Executing API Method", fmt.Errorf("%v", r), "method", req.Method, "id", req.ID, "stack", debug.Stack())
|
||||
slog.Error("Recovered Panic Executing API Method", fmt.Errorf("%v", r), "method", req.Method, "params", fmt.Sprintf("%+v", params[2]), "id", req.ID, "stack", debug.Stack())
|
||||
respondError(w, req.ID, ErrInternalError, fmt.Errorf("%v", r))
|
||||
}
|
||||
}()
|
||||
res := method.handlerFunc.Call(params)
|
||||
|
@ -89,8 +90,9 @@ func (h *Handler) HandleRequest(ctx context.Context, s *session.Session, r io.Re
|
|||
|
||||
if !res[1].IsNil() {
|
||||
reqerr := res[1].Interface().(error)
|
||||
slog.Error("API Method", reqerr, "method", req.Method, "id", req.ID)
|
||||
slog.Error("API Method", reqerr, "method", req.Method, "id", req.ID, "params", fmt.Sprintf("%+v", params[2]))
|
||||
respondError(w, req.ID, ErrInternalError, reqerr)
|
||||
return nil
|
||||
}
|
||||
|
||||
respondResult(w, req.ID, result)
|
||||
|
|
75
internal/networkd/apply.go
Normal file
75
internal/networkd/apply.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package networkd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
const basepath = "/etc/systemd/network"
|
||||
|
||||
func ApplyNetworkdConfiguration(currentConfig definitions.Config, pendingConfig definitions.Config) error {
|
||||
files, err := GenerateNetworkdConfiguration(pendingConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Generating Networkd Configuration: %w", err)
|
||||
}
|
||||
|
||||
err = RemoveContents(basepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Removing old Config Files: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
f, err := os.Create(basepath + "/" + file.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating File: %w", err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(file.Content + "\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing File: %w", err)
|
||||
}
|
||||
|
||||
err = f.Sync()
|
||||
if err != nil {
|
||||
return fmt.Errorf("syncing File: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("systemctl", "restart", "systemd-networkd")
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("restarting networkd: %w", err)
|
||||
}
|
||||
slog.Info("networkd output", "out", out.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RemoveContents(dir string) error {
|
||||
d, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer d.Close()
|
||||
names, err := d.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range names {
|
||||
err = os.RemoveAll(filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
171
internal/networkd/configuration.go
Normal file
171
internal/networkd/configuration.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
package networkd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/exp/slog"
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
)
|
||||
|
||||
type NetworkdConfigFile struct {
|
||||
Name string
|
||||
Content string
|
||||
}
|
||||
|
||||
type InterfaceWithName struct {
|
||||
Name string
|
||||
definitions.Interface
|
||||
Vlans []string
|
||||
StaticRoutes []definitions.StaticRoute
|
||||
}
|
||||
|
||||
type BondMembership struct {
|
||||
Name string
|
||||
BondName string
|
||||
}
|
||||
|
||||
type BridgeMembership struct {
|
||||
Name string
|
||||
BridgeName string
|
||||
}
|
||||
|
||||
func GenerateNetworkdConfiguration(conf definitions.Config) ([]NetworkdConfigFile, error) {
|
||||
files := []NetworkdConfigFile{}
|
||||
|
||||
// Step 1 Generate vlan netdev files
|
||||
for name, inter := range conf.Network.Interfaces {
|
||||
if inter.Type == definitions.Vlan {
|
||||
buf := new(bytes.Buffer)
|
||||
err := templates.ExecuteTemplate(buf, "create-vlan.netdev.tmpl", InterfaceWithName{
|
||||
Name: name,
|
||||
Interface: inter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing create-vlan.netdev.tmpl template: %w", err)
|
||||
}
|
||||
files = append(files, NetworkdConfigFile{
|
||||
Name: fmt.Sprintf("10-create-vlan-%v.netdev", name),
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2 Generate bond netdev files
|
||||
for name, inter := range conf.Network.Interfaces {
|
||||
if inter.Type == definitions.Bond {
|
||||
buf := new(bytes.Buffer)
|
||||
err := templates.ExecuteTemplate(buf, "create-bond.netdev.tmpl", InterfaceWithName{
|
||||
Name: name,
|
||||
Interface: inter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing create-bond.netdev.tmpl template: %w", err)
|
||||
}
|
||||
files = append(files, NetworkdConfigFile{
|
||||
Name: fmt.Sprintf("20-create-bond-%v.netdev", name),
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3 Generate bridge netdev files
|
||||
for name, inter := range conf.Network.Interfaces {
|
||||
if inter.Type == definitions.Bridge {
|
||||
buf := new(bytes.Buffer)
|
||||
err := templates.ExecuteTemplate(buf, "create-bridge.netdev.tmpl", InterfaceWithName{
|
||||
Name: name,
|
||||
Interface: inter,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing create-bridge.netdev.tmpl template: %w", err)
|
||||
}
|
||||
files = append(files, NetworkdConfigFile{
|
||||
Name: fmt.Sprintf("30-create-bridge-%v.netdev", name),
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4 Generate Bond Members
|
||||
for name, inter := range conf.Network.Interfaces {
|
||||
if inter.Type == definitions.Bond && inter.BondMembers != nil {
|
||||
for _, member := range *inter.BondMembers {
|
||||
buf := new(bytes.Buffer)
|
||||
err := templates.ExecuteTemplate(buf, "bond-membership.network.tmpl", BondMembership{
|
||||
Name: member,
|
||||
BondName: name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing bond-membership.network.tmpl template: %w", err)
|
||||
}
|
||||
files = append(files, NetworkdConfigFile{
|
||||
Name: fmt.Sprintf("40-bond-membership-%v.network", name),
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5 Generate Bridge Members
|
||||
for name, inter := range conf.Network.Interfaces {
|
||||
if inter.Type == definitions.Bridge && inter.BridgeMembers != nil {
|
||||
for _, member := range *inter.BridgeMembers {
|
||||
buf := new(bytes.Buffer)
|
||||
err := templates.ExecuteTemplate(buf, "bridge-membership.network.tmpl", BridgeMembership{
|
||||
Name: member,
|
||||
BridgeName: name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing bridge-membership.network.tmpl template: %w", err)
|
||||
}
|
||||
files = append(files, NetworkdConfigFile{
|
||||
Name: fmt.Sprintf("50-bridge-membership-%v.network", name),
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6 Generate addressing network files
|
||||
for name, inter := range conf.Network.Interfaces {
|
||||
// Vlans
|
||||
vlans := []string{}
|
||||
if inter.Type != definitions.Vlan {
|
||||
vlans := []string{}
|
||||
for vlanName, vlanInter := range conf.Network.Interfaces {
|
||||
if vlanInter.Type == definitions.Vlan {
|
||||
if *vlanInter.VlanParent == name {
|
||||
vlans = append(vlans, vlanName)
|
||||
}
|
||||
}
|
||||
}
|
||||
slog.Info("Vlans on interface", "interface", name, "count", len(vlans))
|
||||
}
|
||||
|
||||
// Static Routes
|
||||
staticRoutes := []definitions.StaticRoute{}
|
||||
for _, route := range conf.Network.StaticRoutes {
|
||||
if route.Interface == name {
|
||||
staticRoutes = append(staticRoutes, route)
|
||||
}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
err := templates.ExecuteTemplate(buf, "config-addressing.network.tmpl", InterfaceWithName{
|
||||
Name: name,
|
||||
Interface: inter,
|
||||
Vlans: vlans,
|
||||
StaticRoutes: staticRoutes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("executing config-addressing.network.tmpl template: %w", err)
|
||||
}
|
||||
files = append(files, NetworkdConfigFile{
|
||||
Name: fmt.Sprintf("60-config-addressing-%v.network", name),
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
48
internal/networkd/dbus/link.go
Normal file
48
internal/networkd/dbus/link.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package dbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/exp/slog"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Name string `json:"name"`
|
||||
CarrierState string `json:"carrier_state"`
|
||||
OperationalState string `json:"operational_state"`
|
||||
}
|
||||
|
||||
func GetLinks(dbusConn dbus.Conn) ([]Link, error) {
|
||||
managerObj := dbusConn.Object("org.freedesktop.network1", dbus.ObjectPath("/org/freedesktop/network1"))
|
||||
|
||||
var links [][]any
|
||||
err := managerObj.Call("org.freedesktop.network1.Manager.ListLinks", 0).Store(&links)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Calling ListLinks %w", err)
|
||||
}
|
||||
slog.Info("Dbus Result", "links", links)
|
||||
|
||||
result := []Link{}
|
||||
|
||||
for _, link := range links {
|
||||
name := link[1].(string)
|
||||
path := link[2].(dbus.ObjectPath)
|
||||
linkObj := dbusConn.Object("org.freedesktop.network1", path)
|
||||
carrierState, err := linkObj.GetProperty("org.freedesktop.network1.Link.CarrierState")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetProperty CarrierState %w", err)
|
||||
}
|
||||
operationalState, err := linkObj.GetProperty("org.freedesktop.network1.Link.OperationalState")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetProperty OperationalState %w", err)
|
||||
}
|
||||
result = append(result, Link{
|
||||
Name: name,
|
||||
CarrierState: carrierState.String(),
|
||||
OperationalState: operationalState.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
19
internal/networkd/template.go
Normal file
19
internal/networkd/template.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package networkd
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
//go:embed template
|
||||
var templateFS embed.FS
|
||||
var templates *template.Template
|
||||
|
||||
func init() {
|
||||
|
||||
var err error
|
||||
templates, err = template.New("").ParseFS(templateFS, "template/*.tmpl")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
9
internal/networkd/template/bond-membership.network.tmpl
Normal file
9
internal/networkd/template/bond-membership.network.tmpl
Normal file
|
@ -0,0 +1,9 @@
|
|||
[Match]
|
||||
{{- if eq .Type 0 }}
|
||||
Name={{ .HardwareDevice }}
|
||||
{{- else }}
|
||||
Name={{ .Name }}
|
||||
{{- end }}
|
||||
|
||||
[Network]
|
||||
Bond={{ .BondName }}
|
|
@ -0,0 +1,9 @@
|
|||
[Match]
|
||||
{{- if eq .Type 0 }}
|
||||
Name={{ .HardwareDevice }}
|
||||
{{- else }}
|
||||
Name={{ .Name }}
|
||||
{{- end }}
|
||||
|
||||
[Network]
|
||||
Bridge={{ .BridgeName }}
|
26
internal/networkd/template/config-addressing.network.tmpl
Normal file
26
internal/networkd/template/config-addressing.network.tmpl
Normal file
|
@ -0,0 +1,26 @@
|
|||
[Match]
|
||||
{{- if eq .Type 0 }}
|
||||
Name={{ .HardwareDevice }}
|
||||
{{- else }}
|
||||
Name={{ .Name }}
|
||||
{{- end }}
|
||||
|
||||
[Network]
|
||||
LLMNR=no
|
||||
{{- if eq .AddressingMode 1 }}
|
||||
Address={{ .Address }}
|
||||
{{- else if eq .AddressingMode 2 }}
|
||||
DHCP=yes
|
||||
{{- end }}
|
||||
{{- range .Vlans }}
|
||||
VLAN={{ . }}
|
||||
{{- end}}
|
||||
|
||||
{{- range .StaticRoutes }}
|
||||
[Route]
|
||||
Destination={{ .Destination }}
|
||||
Gateway={{ .Gateway }}
|
||||
{{- if ne .Metric 0 }}
|
||||
Metric={{ .Metric }}
|
||||
{{- end }}
|
||||
{{end}}
|
6
internal/networkd/template/create-bond.netdev.tmpl
Normal file
6
internal/networkd/template/create-bond.netdev.tmpl
Normal file
|
@ -0,0 +1,6 @@
|
|||
[NetDev]
|
||||
Name={{ .Name }}
|
||||
Kind=bond
|
||||
|
||||
[Bond]
|
||||
Mode=active-backup
|
3
internal/networkd/template/create-bridge.netdev.tmpl
Normal file
3
internal/networkd/template/create-bridge.netdev.tmpl
Normal file
|
@ -0,0 +1,3 @@
|
|||
[NetDev]
|
||||
Name={{ .Name }}
|
||||
Kind=bridge
|
6
internal/networkd/template/create-vlan.netdev.tmpl
Normal file
6
internal/networkd/template/create-vlan.netdev.tmpl
Normal file
|
@ -0,0 +1,6 @@
|
|||
[NetDev]
|
||||
Name={{ .Name }}
|
||||
Kind=vlan
|
||||
|
||||
[VLAN]
|
||||
Id={{ .VlanID }}
|
|
@ -77,8 +77,8 @@ func GenerateAddressMatcher(allAddresses map[string]definitions.Address, match d
|
|||
sourceAddresses = append(sourceAddresses, address.Host.String())
|
||||
case definitions.Range:
|
||||
sourceAddresses = append(sourceAddresses, address.Range.String())
|
||||
case definitions.Network:
|
||||
sourceAddresses = append(sourceAddresses, address.Network.String())
|
||||
case definitions.NetworkAddress:
|
||||
sourceAddresses = append(sourceAddresses, address.NetworkAddress.String())
|
||||
default:
|
||||
panic("invalid address type")
|
||||
}
|
||||
|
@ -90,8 +90,8 @@ func GenerateAddressMatcher(allAddresses map[string]definitions.Address, match d
|
|||
destinationAddresses = append(destinationAddresses, address.Host.String())
|
||||
case definitions.Range:
|
||||
destinationAddresses = append(destinationAddresses, address.Range.String())
|
||||
case definitions.Network:
|
||||
destinationAddresses = append(destinationAddresses, address.Network.String())
|
||||
case definitions.NetworkAddress:
|
||||
destinationAddresses = append(destinationAddresses, address.NetworkAddress.String())
|
||||
default:
|
||||
panic("invalid address type")
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"golang.org/x/exp/slog"
|
||||
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
"nfsense.net/nfsense/internal/config"
|
||||
"nfsense.net/nfsense/internal/jsonrpc"
|
||||
"nfsense.net/nfsense/internal/session"
|
||||
)
|
||||
|
@ -18,7 +18,7 @@ var mux = http.NewServeMux()
|
|||
var apiHandler *jsonrpc.Handler
|
||||
var stopCleanup chan struct{}
|
||||
|
||||
func StartWebserver(conf *definitions.Config, _apiHandler *jsonrpc.Handler) {
|
||||
func StartWebserver(configManager *config.ConfigManager, _apiHandler *jsonrpc.Handler) {
|
||||
server.Addr = ":8080"
|
||||
server.Handler = mux
|
||||
apiHandler = _apiHandler
|
||||
|
|
87
main.go
87
main.go
|
@ -2,20 +2,22 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/exp/slog"
|
||||
configAPI "nfsense.net/nfsense/internal/api/config"
|
||||
"nfsense.net/nfsense/internal/api/firewall"
|
||||
"nfsense.net/nfsense/internal/api/network"
|
||||
"nfsense.net/nfsense/internal/api/object"
|
||||
"nfsense.net/nfsense/internal/definitions"
|
||||
"nfsense.net/nfsense/internal/config"
|
||||
"nfsense.net/nfsense/internal/jsonrpc"
|
||||
"nfsense.net/nfsense/internal/nftables"
|
||||
"nfsense.net/nfsense/internal/networkd"
|
||||
"nfsense.net/nfsense/internal/server"
|
||||
)
|
||||
|
||||
|
@ -25,27 +27,41 @@ func main() {
|
|||
|
||||
slog.Info("Starting...")
|
||||
|
||||
conf, err := LoadConfiguration("config.json")
|
||||
dbusConn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
slog.Error("Loading Config", err)
|
||||
slog.Error("Connecting to DBus", err)
|
||||
// os.Exit(1)
|
||||
}
|
||||
defer dbusConn.Close()
|
||||
|
||||
configManager := config.CreateConfigManager()
|
||||
configManager.RegisterApplyFunction(networkd.ApplyNetworkdConfiguration)
|
||||
|
||||
err = configManager.LoadCurrentConfigFromDisk()
|
||||
if err != nil {
|
||||
slog.Error("Loading Current Config", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("Config Loaded", "config", conf)
|
||||
slog.Info("Config Loaded")
|
||||
|
||||
err = definitions.ValidateConfig(conf)
|
||||
err = configManager.LoadPendingConfigFromDisk()
|
||||
if err != nil {
|
||||
slog.Error("Validating Config", err)
|
||||
os.Exit(1)
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
slog.Error("Loading Pending Config", err)
|
||||
}
|
||||
err = configManager.DiscardPendingConfig()
|
||||
if err != nil {
|
||||
slog.Error("Discarding Pending Config", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("Validating Config...")
|
||||
|
||||
if *applyPtr {
|
||||
slog.Info("Applying Config...")
|
||||
err := apply(conf)
|
||||
err := configManager.ApplyPendingChanges()
|
||||
if err != nil {
|
||||
slog.Error("Applying Config", err)
|
||||
slog.Error("Applying Pending Config", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.Info("Config Applied, Exiting...")
|
||||
|
@ -54,10 +70,10 @@ func main() {
|
|||
|
||||
slog.Info("Setup API...")
|
||||
apiHandler := jsonrpc.NewHandler(100 << 20)
|
||||
RegisterAPIMethods(apiHandler, conf)
|
||||
RegisterAPIMethods(apiHandler, configManager, dbusConn)
|
||||
|
||||
slog.Info("Starting Webserver...")
|
||||
server.StartWebserver(conf, apiHandler)
|
||||
server.StartWebserver(configManager, apiHandler)
|
||||
|
||||
slog.Info("Ready.")
|
||||
|
||||
|
@ -75,38 +91,9 @@ func main() {
|
|||
slog.Info("Done")
|
||||
}
|
||||
|
||||
func LoadConfiguration(file string) (*definitions.Config, error) {
|
||||
var config definitions.Config
|
||||
configFile, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening Config File %w", err)
|
||||
}
|
||||
defer configFile.Close()
|
||||
|
||||
jsonParser := json.NewDecoder(configFile)
|
||||
jsonParser.DisallowUnknownFields()
|
||||
err = jsonParser.Decode(&config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding Config File %w", err)
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func RegisterAPIMethods(apiHandler *jsonrpc.Handler, conf *definitions.Config) {
|
||||
apiHandler.Register("Firewall", &firewall.Firewall{Conf: conf})
|
||||
apiHandler.Register("Object", &object.Object{Conf: conf})
|
||||
}
|
||||
|
||||
func apply(conf *definitions.Config) error {
|
||||
fileContent, err := nftables.GenerateNfTablesFile(*conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Generating nftables file %w", err)
|
||||
}
|
||||
|
||||
err = nftables.ApplyNfTablesFile(fileContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Applying nftables %w", err)
|
||||
}
|
||||
slog.Info("Wrote nftables File!")
|
||||
return nil
|
||||
func RegisterAPIMethods(apiHandler *jsonrpc.Handler, configManager *config.ConfigManager, dbusConn *dbus.Conn) {
|
||||
apiHandler.Register("Config", &configAPI.Config{ConfigManager: configManager})
|
||||
apiHandler.Register("Firewall", &firewall.Firewall{ConfigManager: configManager})
|
||||
apiHandler.Register("Network", &network.Network{ConfigManager: configManager, DbusConn: dbusConn})
|
||||
apiHandler.Register("Object", &object.Object{ConfigManager: configManager})
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue