peer creation and deletion

This commit is contained in:
Christoph Haas 2023-06-23 19:24:59 +02:00
parent e38c48bede
commit 4a53a5207a
21 changed files with 760 additions and 242 deletions

View File

@ -69,7 +69,6 @@ func main() {
backend, err := app.New(cfg, eventBus, authenticator, userManager, wireGuardManager, backend, err := app.New(cfg, eventBus, authenticator, userManager, wireGuardManager,
statisticsCollector, templateManager) statisticsCollector, templateManager)
internal.AssertNoError(err) internal.AssertNoError(err)
err = backend.Startup(ctx) err = backend.Startup(ctx)
internal.AssertNoError(err) internal.AssertNoError(err)

View File

@ -5,8 +5,10 @@ import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from 'vue3-tags-input'; import Vue3TagsInput from 'vue3-tags-input';
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
import isCidr from "is-cidr"; import isCidr from "is-cidr";
import {isIP} from 'is-ip'; import {isIP} from 'is-ip';
import { freshInterface } from '@/helpers/models';
const { t } = useI18n() const { t } = useI18n()
@ -34,54 +36,10 @@ const title = computed(() => {
return t("interfaces.interface.new") return t("interfaces.interface.new")
}) })
const formData = ref(freshFormData()) const formData = ref(freshInterface())
// functions // functions
function freshFormData() {
return {
Disabled: false,
DisplayName: "",
Identifier: "",
Mode: "server",
PublicKey: "",
PrivateKey: "",
ListenPort: 51820,
Addresses: [],
DnsStr: [],
DnsSearch: [],
Mtu: 0,
FirewallMark: 0,
RoutingTable: "",
PreUp: "",
PostUp: "",
PreDown: "",
PostDown: "",
SaveConfig: false,
// Peer defaults
PeerDefNetwork: [],
PeerDefDns: [],
PeerDefDnsSearch: [],
PeerDefEndpoint: "",
PeerDefAllowedIPs: [],
PeerDefMtu: 0,
PeerDefPersistentKeepalive: 0,
PeerDefFirewallMark: 0,
PeerDefRoutingTable: "",
PeerDefPreUp: "",
PeerDefPostUp: "",
PeerDefPreDown: "",
PeerDefPostDown: ""
}
}
watch(() => props.visible, async (newValue, oldValue) => { watch(() => props.visible, async (newValue, oldValue) => {
if (oldValue === false && newValue === true) { // if modal is shown if (oldValue === false && newValue === true) { // if modal is shown
console.log(selectedInterface.value) console.log(selectedInterface.value)
@ -170,7 +128,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
) )
function close() { function close() {
formData.value = freshFormData() formData.value = freshInterface()
emit('close') emit('close')
} }
@ -264,20 +222,7 @@ function handleChangePeerDefDns(tags) {
} }
function handleChangePeerDefDnsSearch(tags) { function handleChangePeerDefDnsSearch(tags) {
formData.value.DnsSearch = tags formData.value.PeerDefDnsSearch = tags
}
function validateCIDR(value) {
return isCidr(value) !== 0
}
function validateIP(value) {
return isIP(value)
}
function validateDomain(value) {
console.log("validating: ", value)
return true
} }
async function save() { async function save() {
@ -289,6 +234,7 @@ async function save() {
} }
close() close()
} catch (e) { } catch (e) {
console.log(e)
notify({ notify({
title: "Backend Connection Failure", title: "Backend Connection Failure",
text: "Failed to save interface!", text: "Failed to save interface!",
@ -302,6 +248,7 @@ async function del() {
await interfaces.DeleteInterface(selectedInterface.value.Identifier) await interfaces.DeleteInterface(selectedInterface.value.Identifier)
close() close()
} catch (e) { } catch (e) {
console.log(e)
notify({ notify({
title: "Backend Connection Failure", title: "Backend Connection Failure",
text: "Failed to delete interface!", text: "Failed to delete interface!",

View File

@ -5,6 +5,11 @@ import {interfaceStore} from "@/stores/interfaces";
import {computed, ref, watch} from "vue"; import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { notify } from "@kyvg/vue3-notification"; import { notify } from "@kyvg/vue3-notification";
import Vue3TagsInput from "vue3-tags-input";
import { validateCIDR, validateIP, validateDomain } from '@/helpers/validators';
import isCidr from "is-cidr";
import {isIP} from 'is-ip';
import { freshPeer } from '@/helpers/models';
const { t } = useI18n() const { t } = useI18n()
@ -52,83 +57,7 @@ const title = computed(() => {
} }
}) })
const formData = ref(freshFormData()) const formData = ref(freshPeer())
function freshFormData() {
return {
Disabled: false,
IgnoreGlobalSettings: true,
Endpoint: {
Value: "",
Overridable: false,
},
AllowedIPsStr: {
Value: "",
Overridable: false,
},
ExtraAllowedIPsStr: "",
PrivateKey: "",
PublicKey: "",
PresharedKey: "",
PersistentKeepalive: {
Value: 0,
Overridable: false,
},
DisplayName: "",
Identifier: "",
UserIdentifier: "",
InterfaceConfig: {
PublicKey: {
Value: "",
Overridable: false,
},
AddressStr: {
Value: "",
Overridable: false,
},
DnsStr: {
Value: "",
Overridable: false,
},
DnsSearchStr: {
Value: "",
Overridable: false,
},
Mtu: {
Value: 0,
Overridable: false,
},
FirewallMark: {
Value: 0,
Overridable: false,
},
RoutingTable: {
Value: "",
Overridable: false,
},
PreUp: {
Value: "",
Overridable: false,
},
PostUp: {
Value: "",
Overridable: false,
},
PreDown: {
Value: "",
Overridable: false,
},
PostDown: {
Value: "",
Overridable: false,
},
}
}
}
// functions // functions
@ -139,14 +68,72 @@ watch(() => props.visible, async (newValue, oldValue) => {
if (!selectedPeer.value) { if (!selectedPeer.value) {
await peers.PreparePeer(selectedInterface.value.Identifier) await peers.PreparePeer(selectedInterface.value.Identifier)
formData.value.Disabled = peers.Prepared.Disabled
formData.value.Identifier = peers.Prepared.Identifier formData.value.Identifier = peers.Prepared.Identifier
formData.value.DisplayName = peers.Prepared.DisplayName formData.value.DisplayName = peers.Prepared.DisplayName
formData.value.UserIdentifier = peers.Prepared.UserIdentifier
formData.value.InterfaceIdentifier = peers.Prepared.InterfaceIdentifier
formData.value.Disabled = peers.Prepared.Disabled
formData.value.ExpiresAt = peers.Prepared.ExpiresAt
formData.value.Notes = peers.Prepared.Notes
formData.value.Endpoint = peers.Prepared.Endpoint
formData.value.EndpointPublicKey = peers.Prepared.EndpointPublicKey
formData.value.AllowedIPs = peers.Prepared.AllowedIPs
formData.value.ExtraAllowedIPs = peers.Prepared.ExtraAllowedIPs
formData.value.PresharedKey = peers.Prepared.PresharedKey
formData.value.PersistentKeepalive = peers.Prepared.PersistentKeepalive
formData.value.PrivateKey = peers.Prepared.PrivateKey
formData.value.PublicKey = peers.Prepared.PublicKey
formData.value.Mode = peers.Prepared.Mode
formData.value.Addresses = peers.Prepared.Addresses
formData.value.CheckAliveAddress = peers.Prepared.CheckAliveAddress
formData.value.Dns = peers.Prepared.Dns
formData.value.DnsSearch = peers.Prepared.DnsSearch
formData.value.Mtu = peers.Prepared.Mtu
formData.value.FirewallMark = peers.Prepared.FirewallMark
formData.value.RoutingTable = peers.Prepared.RoutingTable
formData.value.PreUp = peers.Prepared.PreUp
formData.value.PostUp = peers.Prepared.PostUp
formData.value.PreDown = peers.Prepared.PreDown
formData.value.PostDown = peers.Prepared.PostDown
} else { // fill existing data } else { // fill existing data
formData.value.Disabled = selectedPeer.value.Disabled
formData.value.Identifier = selectedPeer.value.Identifier formData.value.Identifier = selectedPeer.value.Identifier
formData.value.DisplayName = selectedPeer.value.DisplayName formData.value.DisplayName = selectedPeer.value.DisplayName
formData.value.UserIdentifier = selectedPeer.value.UserIdentifier
formData.value.InterfaceIdentifier = selectedPeer.value.InterfaceIdentifier
formData.value.Disabled = selectedPeer.value.Disabled
formData.value.ExpiresAt = selectedPeer.value.ExpiresAt
formData.value.Notes = selectedPeer.value.Notes
formData.value.Endpoint = selectedPeer.value.Endpoint
formData.value.EndpointPublicKey = selectedPeer.value.EndpointPublicKey
formData.value.AllowedIPs = selectedPeer.value.AllowedIPs
formData.value.ExtraAllowedIPs = selectedPeer.value.ExtraAllowedIPs
formData.value.PresharedKey = selectedPeer.value.PresharedKey
formData.value.PersistentKeepalive = selectedPeer.value.PersistentKeepalive
formData.value.PrivateKey = selectedPeer.value.PrivateKey
formData.value.PublicKey = selectedPeer.value.PublicKey
formData.value.Mode = selectedPeer.value.Mode
formData.value.Addresses = selectedPeer.value.Addresses
formData.value.CheckAliveAddress = selectedPeer.value.CheckAliveAddress
formData.value.Dns = selectedPeer.value.Dns
formData.value.DnsSearch = selectedPeer.value.DnsSearch
formData.value.Mtu = selectedPeer.value.Mtu
formData.value.FirewallMark = selectedPeer.value.FirewallMark
formData.value.RoutingTable = selectedPeer.value.RoutingTable
formData.value.PreUp = selectedPeer.value.PreUp
formData.value.PostUp = selectedPeer.value.PostUp
formData.value.PreDown = selectedPeer.value.PreDown
formData.value.PostDown = selectedPeer.value.PostDown
} }
} }
@ -154,10 +141,114 @@ watch(() => props.visible, async (newValue, oldValue) => {
) )
function close() { function close() {
formData.value = freshFormData() formData.value = freshPeer()
emit('close') emit('close')
} }
function handleChangeAddresses(tags) {
let validInput = true
tags.forEach(tag => {
if(isCidr(tag) === 0) {
validInput = false
notify({
title: "Invalid CIDR",
text: tag + " is not a valid IP address",
type: 'error',
})
}
})
if(validInput) {
formData.value.Addresses = tags
}
}
function handleChangeAllowedIPs(tags) {
let validInput = true
tags.forEach(tag => {
if(isCidr(tag) === 0) {
validInput = false
notify({
title: "Invalid CIDR",
text: tag + " is not a valid IP address",
type: 'error',
})
}
})
if(validInput) {
formData.value.AllowedIPs = tags
}
}
function handleChangeExtraAllowedIPs(tags) {
let validInput = true
tags.forEach(tag => {
if(isCidr(tag) === 0) {
validInput = false
notify({
title: "Invalid CIDR",
text: tag + " is not a valid IP address",
type: 'error',
})
}
})
if(validInput) {
formData.value.ExtraAllowedIPs = tags
}
}
function handleChangeDns(tags) {
let validInput = true
tags.forEach(tag => {
if(!isIP(tag)) {
validInput = false
notify({
title: "Invalid IP",
text: tag + " is not a valid IP address",
type: 'error',
})
}
})
if(validInput) {
formData.value.Dns = tags
}
}
function handleChangeDnsSearch(tags) {
formData.value.DnsSearch = tags
}
async function save() {
try {
if (props.peerId!=='#NEW#') {
await peers.UpdatePeer(selectedPeer.value.Identifier, formData.value)
} else {
await peers.CreatePeer(selectedInterface.value.Identifier, formData.value)
}
close()
} catch (e) {
console.log(e)
notify({
title: "Backend Connection Failure",
text: "Failed to save peer!",
type: 'error',
})
}
}
async function del() {
try {
await peers.DeletePeer(selectedPeer.value.Identifier)
close()
} catch (e) {
console.log(e)
notify({
title: "Backend Connection Failure",
text: "Failed to delete peer!",
type: 'error',
})
}
}
</script> </script>
<template> <template>
@ -188,6 +279,10 @@ function close() {
<label class="form-label mt-4">{{ $t('modals.peeredit.presharedkey') }}</label> <label class="form-label mt-4">{{ $t('modals.peeredit.presharedkey') }}</label>
<input type="email" class="form-control" placeholder="Optional pre-shared key" v-model="formData.PresharedKey"> <input type="email" class="form-control" placeholder="Optional pre-shared key" v-model="formData.PresharedKey">
</div> </div>
<div class="form-group" v-if="formData.Mode==='client'">
<label class="form-label mt-4">{{ $t('modals.peeredit.endpointpublickey') }}</label>
<input type="text" class="form-control" placeholder="Endpoint Public Key" v-model="formData.EndpointPublicKey.Value">
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend class="mt-4">Networking</legend> <legend class="mt-4">Networking</legend>
@ -197,19 +292,43 @@ function close() {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peeredit.ips') }}</label> <label class="form-label mt-4">{{ $t('modals.peeredit.ips') }}</label>
<input type="text" class="form-control" placeholder="Client IP Address" v-model="formData.InterfaceConfig.AddressStr.Value"> <vue3-tags-input class="form-control" :tags="formData.Addresses"
placeholder="IP Addresses (CIDR format)"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateCIDR"
@on-tags-changed="handleChangeAddresses"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peeredit.allowedips') }}</label> <label class="form-label mt-4">{{ $t('modals.peeredit.allowedips') }}</label>
<input type="text" class="form-control" placeholder="Allowed IP Address" v-model="formData.AllowedIPsStr.Value"> <vue3-tags-input class="form-control" :tags="formData.AllowedIPs.Value"
placeholder="Allowed IP Addresses (CIDR format)"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateCIDR"
@on-tags-changed="handleChangeAllowedIPs"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peeredit.extraallowedips') }}</label> <label class="form-label mt-4">{{ $t('modals.peeredit.extraallowedips') }}</label>
<input type="text" class="form-control" placeholder="Extra Allowed IP's (Server Sided)" v-model="formData.ExtraAllowedIPsStr.Value"> <vue3-tags-input class="form-control" :tags="formData.ExtraAllowedIPs"
placeholder="Extra allowed IP's (Server Sided)"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateCIDR"
@on-tags-changed="handleChangeExtraAllowedIPs"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peeredit.dns') }}</label> <label class="form-label mt-4">{{ $t('modals.peeredit.dns') }}</label>
<input type="text" class="form-control" placeholder="Client DNS Servers" v-model="formData.InterfaceConfig.DnsStr.Value"> <vue3-tags-input class="form-control" :tags="formData.Dns.Value"
placeholder="DNS Servers"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateIP"
@on-tags-changed="handleChangeDns"/>
</div>
<div class="form-group">
<label class="form-label mt-4">{{ $t('modals.peeredit.dnssearch') }}</label>
<vue3-tags-input class="form-control" :tags="formData.DnsSearch.Value"
placeholder="DNS Search prefixes"
:add-tag-on-keys="[13, 188, 32, 9]"
:validate="validateDomain"
@on-tags-changed="handleChangeDnsSearch"/>
</div> </div>
<div class="row"> <div class="row">
<div class="form-group col-md-6"> <div class="form-group col-md-6">
@ -218,7 +337,7 @@ function close() {
</div> </div>
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label class="form-label mt-4">{{ $t('modals.peeredit.mtu') }}</label> <label class="form-label mt-4">{{ $t('modals.peeredit.mtu') }}</label>
<input type="number" class="form-control" placeholder="Client MTU (0 = default)" v-model="formData.InterfaceConfig.Mtu.Value"> <input type="number" class="form-control" placeholder="Client MTU (0 = default)" v-model="formData.Mtu.Value">
</div> </div>
</div> </div>
</fieldset> </fieldset>
@ -228,18 +347,14 @@ function close() {
<input class="form-check-input" type="checkbox" v-model="formData.Disabled"> <input class="form-check-input" type="checkbox" v-model="formData.Disabled">
<label class="form-check-label" >Disabled</label> <label class="form-check-label" >Disabled</label>
</div> </div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" checked="" v-model="formData.IgnoreGlobalSettings">
<label class="form-check-label">Ignore global settings</label>
</div>
</fieldset> </fieldset>
</template> </template>
<template #footer> <template #footer>
<div class="flex-fill text-start"> <div class="flex-fill text-start">
<button type="button" class="btn btn-danger me-1">Delete</button> <button v-if="props.peerId!=='#NEW#'" class="btn btn-danger me-1" type="button" @click.prevent="del">Delete</button>
</div> </div>
<button type="button" class="btn btn-primary me-1">Save</button> <button class="btn btn-primary me-1" type="button" @click.prevent="save">Save</button>
<button @click.prevent="close" type="button" class="btn btn-secondary">Discard</button> <button class="btn btn-secondary" type="button" @click.prevent="close">Discard</button>
</template> </template>
</Modal> </Modal>
</template> </template>

View File

@ -0,0 +1,120 @@
export function freshInterface() {
return {
Disabled: false,
DisplayName: "",
Identifier: "",
Mode: "server",
PublicKey: "",
PrivateKey: "",
ListenPort: 51820,
Addresses: [],
DnsStr: [],
DnsSearch: [],
Mtu: 0,
FirewallMark: 0,
RoutingTable: "",
PreUp: "",
PostUp: "",
PreDown: "",
PostDown: "",
SaveConfig: false,
// Peer defaults
PeerDefNetwork: [],
PeerDefDns: [],
PeerDefDnsSearch: [],
PeerDefEndpoint: "",
PeerDefAllowedIPs: [],
PeerDefMtu: 0,
PeerDefPersistentKeepalive: 0,
PeerDefFirewallMark: 0,
PeerDefRoutingTable: "",
PeerDefPreUp: "",
PeerDefPostUp: "",
PeerDefPreDown: "",
PeerDefPostDown: ""
}
}
export function freshPeer() {
return {
Identifier: "",
DisplayName: "",
UserIdentifier: "",
InterfaceIdentifier: "",
Disabled: false,
ExpiresAt: null,
Notes: "",
Endpoint: {
Value: "",
Overridable: true,
},
EndpointPublicKey: {
Value: "",
Overridable: true,
},
AllowedIPs: {
Value: [],
Overridable: true,
},
ExtraAllowedIPs: [],
PresharedKey: "",
PersistentKeepalive: {
Value: 0,
Overridable: true,
},
PrivateKey: "",
PublicKey: "",
Mode: "client",
Addresses: [],
CheckAliveAddress: "",
Dns: {
Value: [],
Overridable: true,
},
DnsSearch: {
Value: [],
Overridable: true,
},
Mtu: {
Value: 0,
Overridable: true,
},
FirewallMark: {
Value: 0,
Overridable: true,
},
RoutingTable: {
Value: "",
Overridable: true,
},
PreUp: {
Value: "",
Overridable: true,
},
PostUp: {
Value: "",
Overridable: true,
},
PreDown: {
Value: "",
Overridable: true,
},
PostDown: {
Value: "",
Overridable: true,
}
}
}

View File

@ -0,0 +1,14 @@
import isCidr from "is-cidr";
import {isIP} from 'is-ip';
export function validateCIDR(value) {
return isCidr(value) !== 0
}
export function validateIP(value) {
return isIP(value)
}
export function validateDomain(value) {
return true
}

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import {apiWrapper} from '@/helpers/fetch-wrapper' import {apiWrapper} from '@/helpers/fetch-wrapper'
import {notify} from "@kyvg/vue3-notification"; import {notify} from "@kyvg/vue3-notification";
import { freshInterface } from '@/helpers/models';
const baseUrl = `/interface` const baseUrl = `/interface`
@ -9,12 +10,9 @@ export const interfaceStore = defineStore({
id: 'interfaces', id: 'interfaces',
state: () => ({ state: () => ({
interfaces: [], interfaces: [],
prepared: { prepared: freshInterface(),
Identifier: "",
Type: "server",
},
configuration: "", configuration: "",
selected: "wg0", selected: "",
fetching: false, fetching: false,
}), }),
getters: { getters: {
@ -30,6 +28,11 @@ export const interfaceStore = defineStore({
actions: { actions: {
setInterfaces(interfaces) { setInterfaces(interfaces) {
this.interfaces = interfaces this.interfaces = interfaces
if (this.interfaces.length > 0) {
this.selected = this.interfaces[0].Identifier
} else {
this.selected = ""
}
this.fetching = false this.fetching = false
}, },
async LoadInterfaces() { async LoadInterfaces() {
@ -55,7 +58,7 @@ export const interfaceStore = defineStore({
return apiWrapper.get(`${baseUrl}/prepare`) return apiWrapper.get(`${baseUrl}/prepare`)
.then(this.setPreparedInterface) .then(this.setPreparedInterface)
.catch(error => { .catch(error => {
this.prepared = {} this.prepared = freshInterface()
console.log("Failed to load prepared interface: ", error) console.log("Failed to load prepared interface: ", error)
notify({ notify({
title: "Backend Connection Failure", title: "Backend Connection Failure",
@ -64,7 +67,7 @@ export const interfaceStore = defineStore({
}) })
}, },
async InterfaceConfig(id) { async InterfaceConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${id}`) return apiWrapper.get(`${baseUrl}/config/${encodeURIComponent(id)}`)
.then(this.setInterfaceConfig) .then(this.setInterfaceConfig)
.catch(error => { .catch(error => {
this.prepared = {} this.prepared = {}
@ -77,9 +80,14 @@ export const interfaceStore = defineStore({
}, },
async DeleteInterface(id) { async DeleteInterface(id) {
this.fetching = true this.fetching = true
return apiWrapper.delete(`${baseUrl}/${id}`) return apiWrapper.delete(`${baseUrl}/${encodeURIComponent(id)}`)
.then(() => { .then(() => {
this.interfaces = this.interfaces.filter(i => i.Identifier !== id) this.interfaces = this.interfaces.filter(i => i.Identifier !== id)
if (this.interfaces.length > 0) {
this.selected = this.interfaces[0].Identifier
} else {
this.selected = ""
}
this.fetching = false this.fetching = false
}) })
.catch(error => { .catch(error => {
@ -90,7 +98,7 @@ export const interfaceStore = defineStore({
}, },
async UpdateInterface(id, formData) { async UpdateInterface(id, formData) {
this.fetching = true this.fetching = true
return apiWrapper.put(`${baseUrl}/${id}`, formData) return apiWrapper.put(`${baseUrl}/${encodeURIComponent(id)}`, formData)
.then(iface => { .then(iface => {
let idx = this.interfaces.findIndex((i) => i.Identifier === id) let idx = this.interfaces.findIndex((i) => i.Identifier === id)
this.interfaces[idx] = iface this.interfaces[idx] = iface

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import {apiWrapper} from "../helpers/fetch-wrapper"; import {apiWrapper} from "../helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification"; import {notify} from "@kyvg/vue3-notification";
import {interfaceStore} from "./interfaces"; import {interfaceStore} from "./interfaces";
import { freshPeer } from '@/helpers/models';
const baseUrl = `/peer` const baseUrl = `/peer`
@ -9,9 +10,8 @@ export const peerStore = defineStore({
id: 'peers', id: 'peers',
state: () => ({ state: () => ({
peers: [], peers: [],
prepared: { peer: freshPeer(),
Identifier: "", prepared: freshPeer(),
},
filter: "", filter: "",
pageSize: 10, pageSize: 10,
pageOffset: 0, pageOffset: 0,
@ -75,14 +75,18 @@ export const peerStore = defineStore({
this.calculatePages() this.calculatePages()
this.fetching = false this.fetching = false
}, },
setPeer(peer) {
this.peer = peer
this.fetching = false
},
setPreparedPeer(peer) { setPreparedPeer(peer) {
this.prepared = peer; this.prepared = peer;
}, },
async PreparePeer(interfaceId) { async PreparePeer(interfaceId) {
return apiWrapper.get(`${baseUrl}/iface/${iface.Identifier}/prepare`) return apiWrapper.get(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/prepare`)
.then(this.setPreparedPeer) .then(this.setPreparedPeer)
.catch(error => { .catch(error => {
this.prepared = {} this.prepared = freshPeer()
console.log("Failed to load prepared peer: ", error) console.log("Failed to load prepared peer: ", error)
notify({ notify({
title: "Backend Connection Failure", title: "Backend Connection Failure",
@ -90,14 +94,70 @@ export const peerStore = defineStore({
}) })
}) })
}, },
async LoadPeers() { async LoadPeer(id) {
let iface = interfaceStore().GetSelected this.fetching = true
if (!iface) { return apiWrapper.get(`${baseUrl}/${encodeURIComponent(id)}`)
return // no interface, nothing to load .then(this.setPeer)
.catch(error => {
this.setPeers([])
console.log("Failed to load peer: ", error)
notify({
title: "Backend Connection Failure",
text: "Failed to load peer!",
})
})
},
async DeletePeer(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${encodeURIComponent(id)}`)
.then(() => {
this.peers = this.peers.filter(p => p.Identifier !== id)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async UpdatePeer(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${encodeURIComponent(id)}`, formData)
.then(peer => {
let idx = this.peers.findIndex((p) => p.Identifier === id)
this.peers[idx] = peer
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async CreatePeer(interfaceId, formData) {
this.fetching = true
return apiWrapper.post(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/new`, formData)
.then(peer => {
this.peers.push(peer)
this.fetching = false
})
.catch(error => {
this.fetching = false
console.log(error)
throw new Error(error)
})
},
async LoadPeers(interfaceId) {
// if no interfaceId is given, use the currently selected interface
if (!interfaceId) {
interfaceId = interfaceStore().GetSelected.Identifier
if (!interfaceId) {
return // no interface, nothing to load
}
} }
this.fetching = true this.fetching = true
return apiWrapper.get(`${baseUrl}/iface/${iface.Identifier}/all`) return apiWrapper.get(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/all`)
.then(this.setPeers) .then(this.setPeers)
.catch(error => { .catch(error => {
this.setPeers([]) this.setPeers([])

View File

@ -79,7 +79,7 @@ export const profileStore = defineStore({
async LoadPeers() { async LoadPeers() {
this.fetching = true this.fetching = true
let currentUser = authStore().user.Identifier let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${currentUser}/peers`) return apiWrapper.get(`${baseUrl}/${encodeURIComponent(currentUser)}/peers`)
.then(this.setPeers) .then(this.setPeers)
.catch(error => { .catch(error => {
this.setPeers([]) this.setPeers([])
@ -93,7 +93,7 @@ export const profileStore = defineStore({
async LoadUser() { async LoadUser() {
this.fetching = true this.fetching = true
let currentUser = authStore().user.Identifier let currentUser = authStore().user.Identifier
return apiWrapper.get(`${baseUrl}/${currentUser}`) return apiWrapper.get(`${baseUrl}/${encodeURIComponent(currentUser)}`)
.then(this.setUser) .then(this.setUser)
.catch(error => { .catch(error => {
this.setUser({}) this.setUser({})

View File

@ -91,7 +91,7 @@ export const userStore = defineStore({
}, },
async DeleteUser(id) { async DeleteUser(id) {
this.fetching = true this.fetching = true
return apiWrapper.delete(`${baseUrl}/` + id) return apiWrapper.delete(`${baseUrl}/${encodeURIComponent(id)}`)
.then(() => { .then(() => {
this.users = this.users.filter(u => u.Identifier !== id) this.users = this.users.filter(u => u.Identifier !== id)
this.fetching = false this.fetching = false
@ -104,7 +104,7 @@ export const userStore = defineStore({
}, },
async UpdateUser(id, formData) { async UpdateUser(id, formData) {
this.fetching = true this.fetching = true
return apiWrapper.put(`${baseUrl}/` + id, formData) return apiWrapper.put(`${baseUrl}/${encodeURIComponent(id)}`, formData)
.then(user => { .then(user => {
let idx = this.users.findIndex((u) => u.Identifier === id) let idx = this.users.findIndex((u) => u.Identifier === id)
this.users[idx] = user this.users[idx] = user
@ -131,7 +131,7 @@ export const userStore = defineStore({
}, },
async LoadUserPeers(id) { async LoadUserPeers(id) {
this.fetching = true this.fetching = true
return apiWrapper.get(`${baseUrl}/${id}/peers`) return apiWrapper.get(`${baseUrl}/${encodeURIComponent(id)}/peers`)
.then(this.setUserPeers) .then(this.setUserPeers)
.catch(error => { .catch(error => {
this.setUserPeers([]) this.setUserPeers([])

View File

@ -289,7 +289,7 @@ onMounted(async () => {
<td> <td>
<span v-for="ip in peer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span> <span v-for="ip in peer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span>
</td> </td>
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint}}</td> <td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
<td>{{peer.LastConnected}}</td> <td>{{peer.LastConnected}}</td>
<td class="text-center"> <td class="text-center">
<a href="#" title="Show peer" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a> <a href="#" title="Show peer" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>

View File

@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"gorm.io/gorm/utils" "gorm.io/gorm/utils"
"os" "os"
@ -317,7 +318,12 @@ func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdenti
return err return err
} }
err = r.db.WithContext(ctx).Delete(&domain.Interface{}, id).Error err = r.db.WithContext(ctx).Delete(&domain.InterfaceStatus{InterfaceId: id}).Error
if err != nil {
return err
}
err = r.db.WithContext(ctx).Debug().Select(clause.Associations).Delete(&domain.Interface{Identifier: id}).Error
if err != nil { if err != nil {
return err return err
} }
@ -359,7 +365,11 @@ func (r *SqlRepo) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIden
func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) { func (r *SqlRepo) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
var peer domain.Peer var peer domain.Peer
err := r.db.WithContext(ctx).Where("identifier = ?", id).Find(&peer).Error err := r.db.WithContext(ctx).Preload("Addresses").First(&peer, id).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return nil, domain.ErrNotFound
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -478,7 +488,19 @@ func (r *SqlRepo) upsertPeer(tx *gorm.DB, peer *domain.Peer) error {
} }
func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error { func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
err := r.db.WithContext(ctx).Delete(&domain.Peer{}, id).Error err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := r.db.WithContext(ctx).Delete(&domain.PeerStatus{PeerId: id}).Error
if err != nil {
return err
}
err = r.db.WithContext(ctx).Select(clause.Associations).Delete(&domain.Peer{Identifier: id}).Error
if err != nil {
return err
}
return nil
})
if err != nil { if err != nil {
return err return err
} }
@ -486,6 +508,66 @@ func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) erro
return nil return nil
} }
func (r *SqlRepo) GetPeerIps(ctx context.Context) (map[domain.PeerIdentifier][]domain.Cidr, error) {
var ips []struct {
domain.Cidr
PeerId domain.PeerIdentifier `gorm:"column:peer_identifier"`
}
err := r.db.WithContext(ctx).
Table("peer_addresses").
Joins("LEFT JOIN cidrs ON peer_addresses.cidr_cidr = cidrs.cidr").
Scan(&ips).Error
if err != nil {
return nil, err
}
result := make(map[domain.PeerIdentifier][]domain.Cidr)
for _, ip := range ips {
result[ip.PeerId] = append(result[ip.PeerId], ip.Cidr)
}
return result, nil
}
func (r *SqlRepo) GetUsedIpsPerSubnet(ctx context.Context) (map[domain.Cidr][]domain.Cidr, error) {
var peerIps []struct {
domain.Cidr
PeerId domain.PeerIdentifier `gorm:"column:peer_identifier"`
}
err := r.db.WithContext(ctx).
Table("peer_addresses").
Joins("LEFT JOIN cidrs ON peer_addresses.cidr_cidr = cidrs.cidr").
Scan(&peerIps).Error
if err != nil {
return nil, fmt.Errorf("failed to fetch peer IP's: %w", err)
}
var interfaceIps []struct {
domain.Cidr
InterfaceId domain.InterfaceIdentifier `gorm:"column:interface_identifier"`
}
err = r.db.WithContext(ctx).
Table("interface_addresses").
Joins("LEFT JOIN cidrs ON interface_addresses.cidr_cidr = cidrs.cidr").
Scan(&interfaceIps).Error
if err != nil {
return nil, fmt.Errorf("failed to fetch interface IP's: %w", err)
}
result := make(map[domain.Cidr][]domain.Cidr)
for _, ip := range interfaceIps {
networkAddr := ip.Cidr.NetworkAddr()
result[networkAddr] = append(result[networkAddr], ip.Cidr)
}
for _, ip := range peerIps {
networkAddr := ip.Cidr.NetworkAddr()
result[networkAddr] = append(result[networkAddr], ip.Cidr)
}
return result, nil
}
// endregion peers // endregion peers
// region users // region users

View File

@ -194,12 +194,12 @@ func (e peerEndpoint) handleUpdatePut() gin.HandlerFunc {
return return
} }
if p.InterfaceIdentifier != peerId { if p.Identifier != peerId {
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "peer id mismatch"}) c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "peer id mismatch"})
return return
} }
updatedPeer, err := e.app.UpdateInterface(ctx, model.NewDomainPeer(&p)) updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()}) c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return return
@ -232,9 +232,7 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id)) err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{ c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
Code: http.StatusInternalServerError, Message: err.Error(),
})
return return
} }

View File

@ -6,8 +6,8 @@ import (
) )
type StringConfigOption struct { type StringConfigOption struct {
Value string `json:"value"` Value string `json:"Value"`
Overridable bool `json:"overridable"` Overridable bool `json:"Overridable"`
} }
func NewStringConfigOption(value string, overridable bool) StringConfigOption { func NewStringConfigOption(value string, overridable bool) StringConfigOption {
@ -32,8 +32,8 @@ func StringConfigOptionToDomain(opt StringConfigOption) domain.StringConfigOptio
} }
type StringSliceConfigOption struct { type StringSliceConfigOption struct {
Value []string `json:"value"` Value []string `json:"Value"`
Overridable bool `json:"overridable"` Overridable bool `json:"Overridable"`
} }
func NewStringSliceConfigOption(value []string, overridable bool) StringSliceConfigOption { func NewStringSliceConfigOption(value []string, overridable bool) StringSliceConfigOption {
@ -58,8 +58,8 @@ func StringSliceConfigOptionToDomain(opt StringSliceConfigOption) domain.StringC
} }
type IntConfigOption struct { type IntConfigOption struct {
Value int `json:"value"` Value int `json:"Value"`
Overridable bool `json:"overridable"` Overridable bool `json:"Overridable"`
} }
func NewIntConfigOption(value int, overridable bool) IntConfigOption { func NewIntConfigOption(value int, overridable bool) IntConfigOption {
@ -84,8 +84,8 @@ func IntConfigOptionToDomain(opt IntConfigOption) domain.IntConfigOption {
} }
type Int32ConfigOption struct { type Int32ConfigOption struct {
Value int32 `json:"value"` Value int32 `json:"Value"`
Overridable bool `json:"overridable"` Overridable bool `json:"Overridable"`
} }
func NewInt32ConfigOption(value int32, overridable bool) Int32ConfigOption { func NewInt32ConfigOption(value int32, overridable bool) Int32ConfigOption {
@ -110,8 +110,8 @@ func Int32ConfigOptionToDomain(opt Int32ConfigOption) domain.Int32ConfigOption {
} }
type BoolConfigOption struct { type BoolConfigOption struct {
Value bool `json:"value"` Value bool `json:"Value"`
Overridable bool `json:"overridable"` Overridable bool `json:"Overridable"`
} }
func NewBoolConfigOption(value bool, overridable bool) BoolConfigOption { func NewBoolConfigOption(value bool, overridable bool) BoolConfigOption {

View File

@ -1,6 +1,6 @@
package model package model
type Error struct { type Error struct {
Code int `json:"code"` Code int `json:"Code"`
Message string `json:"message"` Message string `json:"Message"`
} }

View File

@ -13,11 +13,11 @@ type Peer struct {
InterfaceIdentifier string `json:"InterfaceIdentifier"` // the interface id InterfaceIdentifier string `json:"InterfaceIdentifier"` // the interface id
Disabled bool `json:"Disabled"` // flag that specifies if the peer is enabled (up) or not (down) Disabled bool `json:"Disabled"` // flag that specifies if the peer is enabled (up) or not (down)
DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled
ExpiresAt *time.Time `json:"column:expires_at"` // expiry dates for peers ExpiresAt *time.Time `json:"ExpiresAt"` // expiry dates for peers
Notes string `json:"notes"` // a note field for peers Notes string `json:"Notes"` // a note field for peers
Endpoint StringConfigOption `json:"Endpoint"` // the endpoint address Endpoint StringConfigOption `json:"Endpoint"` // the endpoint address
EndpointPublicKey string `json:"EndpointPublicKey"` // the endpoint public key EndpointPublicKey StringConfigOption `json:"EndpointPublicKey"` // the endpoint public key
AllowedIPs StringSliceConfigOption `json:"AllowedIPs"` // all allowed ip subnets, comma seperated AllowedIPs StringSliceConfigOption `json:"AllowedIPs"` // all allowed ip subnets, comma seperated
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"` // all allowed ip subnets on the server side, comma seperated ExtraAllowedIPs []string `json:"ExtraAllowedIPs"` // all allowed ip subnets on the server side, comma seperated
PresharedKey string `json:"PresharedKey"` // the pre-shared Key of the peer PresharedKey string `json:"PresharedKey"` // the pre-shared Key of the peer
@ -53,7 +53,7 @@ func NewPeer(src *domain.Peer) *Peer {
ExpiresAt: src.ExpiresAt, ExpiresAt: src.ExpiresAt,
Notes: src.Notes, Notes: src.Notes,
Endpoint: StringConfigOptionFromDomain(src.Endpoint), Endpoint: StringConfigOptionFromDomain(src.Endpoint),
EndpointPublicKey: src.EndpointPublicKey, EndpointPublicKey: StringConfigOptionFromDomain(src.EndpointPublicKey),
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr), AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr), ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
PresharedKey: string(src.PresharedKey), PresharedKey: string(src.PresharedKey),
@ -92,7 +92,7 @@ func NewDomainPeer(src *Peer) *domain.Peer {
res := &domain.Peer{ res := &domain.Peer{
BaseModel: domain.BaseModel{}, BaseModel: domain.BaseModel{},
Endpoint: StringConfigOptionToDomain(src.Endpoint), Endpoint: StringConfigOptionToDomain(src.Endpoint),
EndpointPublicKey: src.EndpointPublicKey, EndpointPublicKey: StringConfigOptionToDomain(src.EndpointPublicKey),
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs), AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs), ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
PresharedKey: domain.PreSharedKey(src.PresharedKey), PresharedKey: domain.PreSharedKey(src.PresharedKey),

View File

@ -320,7 +320,9 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
Endpoint: domain.StringConfigOption{ Endpoint: domain.StringConfigOption{
Value: oldPeer.Endpoint, Overridable: !oldPeer.IgnoreGlobalSettings, Value: oldPeer.Endpoint, Overridable: !oldPeer.IgnoreGlobalSettings,
}, },
EndpointPublicKey: iface.PublicKey, EndpointPublicKey: domain.StringConfigOption{
Value: iface.PublicKey, Overridable: !oldPeer.IgnoreGlobalSettings,
},
AllowedIPsStr: domain.StringConfigOption{ AllowedIPsStr: domain.StringConfigOption{
Value: oldPeer.AllowedIPsStr, Overridable: !oldPeer.IgnoreGlobalSettings, Value: oldPeer.AllowedIPsStr, Overridable: !oldPeer.IgnoreGlobalSettings,
}, },
@ -333,7 +335,6 @@ func migrateV1Peers(oldDb, newDb *gorm.DB) error {
Identifier: domain.PeerIdentifier(oldPeer.PublicKey), Identifier: domain.PeerIdentifier(oldPeer.PublicKey),
UserIdentifier: user.Identifier, UserIdentifier: user.Identifier,
InterfaceIdentifier: iface.Identifier, InterfaceIdentifier: iface.Identifier,
Temporary: nil,
Disabled: disableTime, Disabled: disableTime,
DisabledReason: disableReason, DisabledReason: disableReason,
ExpiresAt: expiryTime, ExpiresAt: expiryTime,

View File

@ -38,8 +38,10 @@ type WireGuardManager interface {
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error) UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
} }
type StatisticsCollector interface { type StatisticsCollector interface {

View File

@ -20,6 +20,7 @@ type InterfaceAndPeerDatabaseRepo interface {
SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error SavePeer(ctx context.Context, id domain.PeerIdentifier, updateFunc func(in *domain.Peer) (*domain.Peer, error)) error
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUsedIpsPerSubnet(ctx context.Context) (map[domain.Cidr][]domain.Cidr, error)
} }
type StatisticsDatabaseRepo interface { type StatisticsDatabaseRepo interface {

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/google/uuid"
"github.com/h44z/wg-portal/internal/app" "github.com/h44z/wg-portal/internal/app"
"time" "time"
@ -154,7 +153,7 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
} }
peer.InterfaceIdentifier = in.Identifier peer.InterfaceIdentifier = in.Identifier
peer.EndpointPublicKey = in.PublicKey peer.EndpointPublicKey = domain.StringConfigOption{Value: in.PublicKey, Overridable: true}
peer.AllowedIPsStr = domain.StringConfigOption{Value: in.PeerDefAllowedIPsStr, Overridable: true} peer.AllowedIPsStr = domain.StringConfigOption{Value: in.PeerDefAllowedIPsStr, Overridable: true}
peer.Interface.Addresses = p.AllowedIPs // use allowed IP's as the peer IP's peer.Interface.Addresses = p.AllowedIPs // use allowed IP's as the peer IP's
peer.Interface.DnsStr = domain.StringConfigOption{Value: in.PeerDefDnsStr, Overridable: true} peer.Interface.DnsStr = domain.StringConfigOption{Value: in.PeerDefDnsStr, Overridable: true}
@ -298,7 +297,7 @@ func (m Manager) PrepareInterface(ctx context.Context) (*domain.Interface, error
return nil, fmt.Errorf("failed to generate new identifier: %w", err) return nil, fmt.Errorf("failed to generate new identifier: %w", err)
} }
ipv4, ipv6, err := m.getFreshIpConfig(ctx) ipv4, ipv6, err := m.getFreshInterfaceIpConfig(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate new ip config: %w", err) return nil, fmt.Errorf("failed to generate new ip config: %w", err)
} }
@ -390,7 +389,7 @@ func (m Manager) getNewInterfaceName(ctx context.Context) (domain.InterfaceIdent
return name, nil return name, nil
} }
func (m Manager) getFreshIpConfig(ctx context.Context) (ipV4, ipV6 domain.Cidr, err error) { func (m Manager) getFreshInterfaceIpConfig(ctx context.Context) (ipV4, ipV6 domain.Cidr, err error) {
ips, err := m.db.GetInterfaceIps(ctx) ips, err := m.db.GetInterfaceIps(ctx)
if err != nil { if err != nil {
err = fmt.Errorf("failed to get existing IP addresses: %w", err) err = fmt.Errorf("failed to get existing IP addresses: %w", err)
@ -401,34 +400,49 @@ func (m Manager) getFreshIpConfig(ctx context.Context) (ipV4, ipV6 domain.Cidr,
ipV4, _ = domain.CidrFromString(m.cfg.Advanced.StartCidrV4) ipV4, _ = domain.CidrFromString(m.cfg.Advanced.StartCidrV4)
ipV6, _ = domain.CidrFromString(m.cfg.Advanced.StartCidrV6) ipV6, _ = domain.CidrFromString(m.cfg.Advanced.StartCidrV6)
netV4 := ipV4.NetworkAddr()
netV6 := ipV6.NetworkAddr()
for { for {
ipV4Conflict := false v4Conflict := false
ipV6Conflict := false v6Conflict := false
for _, usedIps := range ips { for _, usedIps := range ips {
for _, ip := range usedIps { for _, usedIp := range usedIps {
if ipV4 == ip { usedNetwork := usedIp.NetworkAddr()
ipV4Conflict = true if netV4 == usedNetwork {
v4Conflict = true
} }
if ipV6 == ip { if netV6 == usedNetwork {
ipV6Conflict = true v6Conflict = true
} }
} }
} }
if !ipV4Conflict && (!useV6 || !ipV6Conflict) { if !v4Conflict && (!useV6 || !v6Conflict) {
break break
} }
if ipV4Conflict { if v4Conflict {
ipV4 = ipV4.NextSubnet() netV4 = netV4.NextSubnet()
} }
if ipV6Conflict && useV6 { if v6Conflict && useV6 {
ipV6 = ipV6.NextSubnet() netV6 = netV6.NextSubnet()
}
if !netV4.IsValid() {
return domain.Cidr{}, domain.Cidr{}, fmt.Errorf("IPv4 space exhausted")
}
if useV6 && !netV6.IsValid() {
return domain.Cidr{}, domain.Cidr{}, fmt.Errorf("IPv6 space exhausted")
} }
} }
// use first address in network for interface
ipV4 = netV4.NextAddr()
ipV6 = netV6.NextAddr()
return return
} }
@ -471,7 +485,7 @@ func (m Manager) CreateInterface(ctx context.Context, in *domain.Interface) (*do
return nil, fmt.Errorf("interface %s already exists", in.Identifier) return nil, fmt.Errorf("interface %s already exists", in.Identifier)
} }
if err := m.validateCreation(ctx, existingInterface, in); err != nil { if err := m.validateInterfaceCreation(ctx, existingInterface, in); err != nil {
return nil, fmt.Errorf("creation not allowed: %w", err) return nil, fmt.Errorf("creation not allowed: %w", err)
} }
@ -501,7 +515,7 @@ func (m Manager) UpdateInterface(ctx context.Context, in *domain.Interface) (*do
return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err) return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
} }
if err := m.validateModifications(ctx, existingInterface, in); err != nil { if err := m.validateInterfaceModifications(ctx, existingInterface, in); err != nil {
return nil, fmt.Errorf("update not allowed: %w", err) return nil, fmt.Errorf("update not allowed: %w", err)
} }
@ -531,7 +545,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
return fmt.Errorf("unable to find interface %s: %w", id, err) return fmt.Errorf("unable to find interface %s: %w", id, err)
} }
if err := m.validateDeletion(ctx, existingInterface); err != nil { if err := m.validateInterfaceDeletion(ctx, existingInterface); err != nil {
return fmt.Errorf("deletion not allowed: %w", err) return fmt.Errorf("deletion not allowed: %w", err)
} }
@ -553,7 +567,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
return nil return nil
} }
func (m Manager) validateModifications(ctx context.Context, old, new *domain.Interface) error { func (m Manager) validateInterfaceModifications(ctx context.Context, old, new *domain.Interface) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin { if !currentUser.IsAdmin {
@ -563,7 +577,7 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Int
return nil return nil
} }
func (m Manager) validateCreation(ctx context.Context, old, new *domain.Interface) error { func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain.Interface) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if new.Identifier == "" { if new.Identifier == "" {
@ -577,7 +591,7 @@ func (m Manager) validateCreation(ctx context.Context, old, new *domain.Interfac
return nil return nil
} }
func (m Manager) validateDeletion(ctx context.Context, del *domain.Interface) error { func (m Manager) validateInterfaceDeletion(ctx context.Context, del *domain.Interface) error {
currentUser := domain.GetUserInfo(ctx) currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin { if !currentUser.IsAdmin {
@ -615,6 +629,11 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
return nil, fmt.Errorf("unable to find interface %s: %w", id, err) return nil, fmt.Errorf("unable to find interface %s: %w", id, err)
} }
ips, err := m.getFreshPeerIpConfig(ctx, iface)
if err != nil {
return nil, fmt.Errorf("unable to get fresh ip addresses: %w", err)
}
kp, err := domain.NewFreshKeypair() kp, err := domain.NewFreshKeypair()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate keys: %w", err) return nil, fmt.Errorf("failed to generate keys: %w", err)
@ -630,7 +649,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
peerMode = domain.InterfaceTypeServer peerMode = domain.InterfaceTypeServer
} }
peerId := domain.PeerIdentifier(uuid.New().String()) peerId := domain.PeerIdentifier(kp.PublicKey)
freshPeer := &domain.Peer{ freshPeer := &domain.Peer{
BaseModel: domain.BaseModel{ BaseModel: domain.BaseModel{
CreatedBy: string(currentUser.Id), CreatedBy: string(currentUser.Id),
@ -639,7 +658,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
Endpoint: domain.NewStringConfigOption(iface.PeerDefEndpoint, true), Endpoint: domain.NewStringConfigOption(iface.PeerDefEndpoint, true),
EndpointPublicKey: iface.PublicKey, EndpointPublicKey: domain.NewStringConfigOption(iface.PublicKey, true),
AllowedIPsStr: domain.NewStringConfigOption(iface.PeerDefAllowedIPsStr, true), AllowedIPsStr: domain.NewStringConfigOption(iface.PeerDefAllowedIPsStr, true),
ExtraAllowedIPsStr: "", ExtraAllowedIPsStr: "",
PresharedKey: pk, PresharedKey: pk,
@ -655,7 +674,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
Interface: domain.PeerInterfaceConfig{ Interface: domain.PeerInterfaceConfig{
KeyPair: kp, KeyPair: kp,
Type: peerMode, Type: peerMode,
Addresses: nil, // TODO Addresses: ips,
CheckAliveAddress: "", CheckAliveAddress: "",
DnsStr: domain.NewStringConfigOption(iface.PeerDefDnsStr, true), DnsStr: domain.NewStringConfigOption(iface.PeerDefDnsStr, true),
DnsSearchStr: domain.NewStringConfigOption(iface.PeerDefDnsSearchStr, true), DnsSearchStr: domain.NewStringConfigOption(iface.PeerDefDnsSearchStr, true),
@ -672,6 +691,121 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
return freshPeer, nil return freshPeer, nil
} }
func (m Manager) getFreshPeerIpConfig(ctx context.Context, iface *domain.Interface) (ips []domain.Cidr, err error) {
networks, err := domain.CidrsFromString(iface.PeerDefNetworkStr)
if err != nil {
err = fmt.Errorf("failed to parse default network address: %w", err)
return
}
existingIps, err := m.db.GetUsedIpsPerSubnet(ctx)
if err != nil {
err = fmt.Errorf("failed to get existing IP addresses: %w", err)
return
}
for _, network := range networks {
ip := network.NextAddr()
for {
ipConflict := false
for _, usedIp := range existingIps[network] {
if usedIp == ip {
ipConflict = true
}
}
if !ipConflict {
break
}
ip = ip.NextAddr()
if !ip.IsValid() {
return nil, fmt.Errorf("ip space on subnet %s is exhausted", network.String())
}
}
ips = append(ips, ip)
}
return
}
func (m Manager) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
peer, err := m.db.GetPeer(ctx, id)
if err != nil {
return nil, fmt.Errorf("unable to find peer %s: %w", id, err)
}
return peer, nil
}
func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
if err != nil && !errors.Is(err, domain.ErrNotFound) {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
}
if existingPeer != nil {
return nil, fmt.Errorf("peer %s already exists", peer.Identifier)
}
if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
return nil, fmt.Errorf("creation not allowed: %w", err)
}
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p)
err = m.wg.SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, peer)
return pp, nil
})
if err != nil {
return nil, fmt.Errorf("failed to create wireguard peer %s: %w", peer.Identifier, err)
}
return peer, nil
})
if err != nil {
return nil, fmt.Errorf("creation failure: %w", err)
}
return peer, nil
}
func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
existingPeer, err := m.db.GetPeer(ctx, peer.Identifier)
if err != nil {
return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
}
if err := m.validatePeerModifications(ctx, existingPeer, peer); err != nil {
return nil, fmt.Errorf("update not allowed: %w", err)
}
err = m.db.SavePeer(ctx, peer.Identifier, func(p *domain.Peer) (*domain.Peer, error) {
peer.CopyCalculatedAttributes(p)
err = m.wg.SavePeer(ctx, peer.InterfaceIdentifier, peer.Identifier,
func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error) {
domain.MergeToPhysicalPeer(pp, peer)
return pp, nil
})
if err != nil {
return nil, fmt.Errorf("failed to update wireguard peer %s: %w", peer.Identifier, err)
}
return peer, nil
})
if err != nil {
return nil, fmt.Errorf("update failure: %w", err)
}
return peer, nil
}
func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error { func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error {
peer, err := m.db.GetPeer(ctx, id) peer, err := m.db.GetPeer(ctx, id)
if err != nil { if err != nil {
@ -691,11 +825,36 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
return nil return nil
} }
func (m Manager) GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) { func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
peer, err := m.db.GetPeer(ctx, id) currentUser := domain.GetUserInfo(ctx)
if err != nil {
return nil, fmt.Errorf("unable to find peer %s: %w", id, err) if !currentUser.IsAdmin {
return fmt.Errorf("insufficient permissions")
} }
return peer, nil return nil
}
func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx)
if new.Identifier == "" {
return fmt.Errorf("invalid peer identifier")
}
if !currentUser.IsAdmin {
return fmt.Errorf("insufficient permissions")
}
return nil
}
func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx)
if !currentUser.IsAdmin {
return fmt.Errorf("insufficient permissions")
}
return nil
} }

View File

@ -21,6 +21,10 @@ func (c Cidr) String() string {
return c.Prefix().String() return c.Prefix().String()
} }
func (c Cidr) IsValid() bool {
return c.Prefix().IsValid()
}
func CidrFromString(str string) (Cidr, error) { func CidrFromString(str string) (Cidr, error) {
prefix, err := netip.ParsePrefix(strings.TrimSpace(str)) prefix, err := netip.ParsePrefix(strings.TrimSpace(str))
if err != nil { if err != nil {
@ -141,16 +145,20 @@ func (c Cidr) NetworkAddr() Cidr {
func (c Cidr) NextAddr() Cidr { func (c Cidr) NextAddr() Cidr {
prefix := c.Prefix() prefix := c.Prefix()
nextAddr := prefix.Addr().Next()
return Cidr{ return Cidr{
Addr: prefix.Addr().Next().String(), Cidr: netip.PrefixFrom(nextAddr, c.NetLength).String(),
Addr: nextAddr.String(),
NetLength: prefix.Bits(), NetLength: prefix.Bits(),
} }
} }
func (c Cidr) NextSubnet() Cidr { func (c Cidr) NextSubnet() Cidr {
prefix := c.Prefix() prefix := c.Prefix()
nextAddr := c.BroadcastAddr().Prefix().Addr().Next()
return Cidr{ return Cidr{
Addr: c.BroadcastAddr().Prefix().Addr().Next().String(), Cidr: netip.PrefixFrom(nextAddr, c.NetLength).String(),
Addr: nextAddr.String(),
NetLength: prefix.Bits(), NetLength: prefix.Bits(),
} }
} }

View File

@ -28,7 +28,7 @@ type Peer struct {
// WireGuard specific (for the [peer] section of the config file) // WireGuard specific (for the [peer] section of the config file)
Endpoint StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_"` // the endpoint address Endpoint StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_"` // the endpoint address
EndpointPublicKey string `gorm:"column:endpoint_pubkey"` // the endpoint public key EndpointPublicKey StringConfigOption `gorm:"embedded;embeddedPrefix:endpoint_pubkey_"` // the endpoint public key
AllowedIPsStr StringConfigOption `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated AllowedIPsStr StringConfigOption `gorm:"embedded;embeddedPrefix:allowed_ips_str_"` // all allowed ip subnets, comma seperated
ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated ExtraAllowedIPsStr string // all allowed ip subnets on the server side, comma seperated
PresharedKey PreSharedKey // the pre-shared Key of the peer PresharedKey PreSharedKey // the pre-shared Key of the peer
@ -49,11 +49,11 @@ type Peer struct {
Interface PeerInterfaceConfig `gorm:"embedded"` Interface PeerInterfaceConfig `gorm:"embedded"`
} }
func (p Peer) IsDisabled() bool { func (p *Peer) IsDisabled() bool {
return p.Disabled != nil return p.Disabled != nil
} }
func (p Peer) CheckAliveAddress() string { func (p *Peer) CheckAliveAddress() string {
if p.Interface.CheckAliveAddress != "" { if p.Interface.CheckAliveAddress != "" {
return p.Interface.CheckAliveAddress return p.Interface.CheckAliveAddress
} }
@ -65,6 +65,10 @@ func (p Peer) CheckAliveAddress() string {
return "" return ""
} }
func (p *Peer) CopyCalculatedAttributes(src *Peer) {
p.BaseModel = src.BaseModel
}
type PeerInterfaceConfig struct { type PeerInterfaceConfig struct {
KeyPair // private/public Key of the peer KeyPair // private/public Key of the peer
@ -149,7 +153,7 @@ func (p PhysicalPeer) GetAllowedIPs() ([]net.IPNet, error) {
func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer { func ConvertPhysicalPeer(pp *PhysicalPeer) *Peer {
peer := &Peer{ peer := &Peer{
Endpoint: StringConfigOption{Value: pp.Endpoint, Overridable: true}, Endpoint: StringConfigOption{Value: pp.Endpoint, Overridable: true},
EndpointPublicKey: "", EndpointPublicKey: StringConfigOption{Value: "", Overridable: true},
AllowedIPsStr: StringConfigOption{Value: "", Overridable: true}, AllowedIPsStr: StringConfigOption{Value: "", Overridable: true},
ExtraAllowedIPsStr: "", ExtraAllowedIPsStr: "",
PresharedKey: pp.PresharedKey, PresharedKey: pp.PresharedKey,