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,
statisticsCollector, templateManager)
internal.AssertNoError(err)
err = backend.Startup(ctx)
internal.AssertNoError(err)

View File

@ -5,8 +5,10 @@ import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n';
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 { freshInterface } from '@/helpers/models';
const { t } = useI18n()
@ -34,54 +36,10 @@ const title = computed(() => {
return t("interfaces.interface.new")
})
const formData = ref(freshFormData())
const formData = ref(freshInterface())
// 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) => {
if (oldValue === false && newValue === true) { // if modal is shown
console.log(selectedInterface.value)
@ -170,7 +128,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
)
function close() {
formData.value = freshFormData()
formData.value = freshInterface()
emit('close')
}
@ -264,20 +222,7 @@ function handleChangePeerDefDns(tags) {
}
function handleChangePeerDefDnsSearch(tags) {
formData.value.DnsSearch = tags
}
function validateCIDR(value) {
return isCidr(value) !== 0
}
function validateIP(value) {
return isIP(value)
}
function validateDomain(value) {
console.log("validating: ", value)
return true
formData.value.PeerDefDnsSearch = tags
}
async function save() {
@ -289,6 +234,7 @@ async function save() {
}
close()
} catch (e) {
console.log(e)
notify({
title: "Backend Connection Failure",
text: "Failed to save interface!",
@ -302,6 +248,7 @@ async function del() {
await interfaces.DeleteInterface(selectedInterface.value.Identifier)
close()
} catch (e) {
console.log(e)
notify({
title: "Backend Connection Failure",
text: "Failed to delete interface!",

View File

@ -5,6 +5,11 @@ import {interfaceStore} from "@/stores/interfaces";
import {computed, ref, watch} from "vue";
import { useI18n } from 'vue-i18n';
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()
@ -52,83 +57,7 @@ const title = computed(() => {
}
})
const formData = ref(freshFormData())
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,
},
}
}
}
const formData = ref(freshPeer())
// functions
@ -139,14 +68,72 @@ watch(() => props.visible, async (newValue, oldValue) => {
if (!selectedPeer.value) {
await peers.PreparePeer(selectedInterface.value.Identifier)
formData.value.Disabled = peers.Prepared.Disabled
formData.value.Identifier = peers.Prepared.Identifier
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
formData.value.Disabled = selectedPeer.value.Disabled
formData.value.Identifier = selectedPeer.value.Identifier
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() {
formData.value = freshFormData()
formData.value = freshPeer()
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>
<template>
@ -188,6 +279,10 @@ function close() {
<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">
</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>
<legend class="mt-4">Networking</legend>
@ -197,19 +292,43 @@ function close() {
</div>
<div class="form-group">
<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 class="form-group">
<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 class="form-group">
<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 class="form-group">
<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 class="row">
<div class="form-group col-md-6">
@ -218,7 +337,7 @@ function close() {
</div>
<div class="form-group col-md-6">
<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>
</fieldset>
@ -228,18 +347,14 @@ function close() {
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
<label class="form-check-label" >Disabled</label>
</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>
</template>
<template #footer>
<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>
<button type="button" class="btn btn-primary me-1">Save</button>
<button @click.prevent="close" type="button" class="btn btn-secondary">Discard</button>
<button class="btn btn-primary me-1" type="button" @click.prevent="save">Save</button>
<button class="btn btn-secondary" type="button" @click.prevent="close">Discard</button>
</template>
</Modal>
</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 {notify} from "@kyvg/vue3-notification";
import { freshInterface } from '@/helpers/models';
const baseUrl = `/interface`
@ -9,12 +10,9 @@ export const interfaceStore = defineStore({
id: 'interfaces',
state: () => ({
interfaces: [],
prepared: {
Identifier: "",
Type: "server",
},
prepared: freshInterface(),
configuration: "",
selected: "wg0",
selected: "",
fetching: false,
}),
getters: {
@ -30,6 +28,11 @@ export const interfaceStore = defineStore({
actions: {
setInterfaces(interfaces) {
this.interfaces = interfaces
if (this.interfaces.length > 0) {
this.selected = this.interfaces[0].Identifier
} else {
this.selected = ""
}
this.fetching = false
},
async LoadInterfaces() {
@ -55,7 +58,7 @@ export const interfaceStore = defineStore({
return apiWrapper.get(`${baseUrl}/prepare`)
.then(this.setPreparedInterface)
.catch(error => {
this.prepared = {}
this.prepared = freshInterface()
console.log("Failed to load prepared interface: ", error)
notify({
title: "Backend Connection Failure",
@ -64,7 +67,7 @@ export const interfaceStore = defineStore({
})
},
async InterfaceConfig(id) {
return apiWrapper.get(`${baseUrl}/config/${id}`)
return apiWrapper.get(`${baseUrl}/config/${encodeURIComponent(id)}`)
.then(this.setInterfaceConfig)
.catch(error => {
this.prepared = {}
@ -77,9 +80,14 @@ export const interfaceStore = defineStore({
},
async DeleteInterface(id) {
this.fetching = true
return apiWrapper.delete(`${baseUrl}/${id}`)
return apiWrapper.delete(`${baseUrl}/${encodeURIComponent(id)}`)
.then(() => {
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
})
.catch(error => {
@ -90,7 +98,7 @@ export const interfaceStore = defineStore({
},
async UpdateInterface(id, formData) {
this.fetching = true
return apiWrapper.put(`${baseUrl}/${id}`, formData)
return apiWrapper.put(`${baseUrl}/${encodeURIComponent(id)}`, formData)
.then(iface => {
let idx = this.interfaces.findIndex((i) => i.Identifier === id)
this.interfaces[idx] = iface

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import {apiWrapper} from "../helpers/fetch-wrapper";
import {notify} from "@kyvg/vue3-notification";
import {interfaceStore} from "./interfaces";
import { freshPeer } from '@/helpers/models';
const baseUrl = `/peer`
@ -9,9 +10,8 @@ export const peerStore = defineStore({
id: 'peers',
state: () => ({
peers: [],
prepared: {
Identifier: "",
},
peer: freshPeer(),
prepared: freshPeer(),
filter: "",
pageSize: 10,
pageOffset: 0,
@ -75,14 +75,18 @@ export const peerStore = defineStore({
this.calculatePages()
this.fetching = false
},
setPeer(peer) {
this.peer = peer
this.fetching = false
},
setPreparedPeer(peer) {
this.prepared = peer;
},
async PreparePeer(interfaceId) {
return apiWrapper.get(`${baseUrl}/iface/${iface.Identifier}/prepare`)
return apiWrapper.get(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/prepare`)
.then(this.setPreparedPeer)
.catch(error => {
this.prepared = {}
this.prepared = freshPeer()
console.log("Failed to load prepared peer: ", error)
notify({
title: "Backend Connection Failure",
@ -90,14 +94,70 @@ export const peerStore = defineStore({
})
})
},
async LoadPeers() {
let iface = interfaceStore().GetSelected
if (!iface) {
return // no interface, nothing to load
async LoadPeer(id) {
this.fetching = true
return apiWrapper.get(`${baseUrl}/${encodeURIComponent(id)}`)
.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
return apiWrapper.get(`${baseUrl}/iface/${iface.Identifier}/all`)
return apiWrapper.get(`${baseUrl}/iface/${encodeURIComponent(interfaceId)}/all`)
.then(this.setPeers)
.catch(error => {
this.setPeers([])

View File

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

View File

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

View File

@ -289,7 +289,7 @@ onMounted(async () => {
<td>
<span v-for="ip in peer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span>
</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 class="text-center">
<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"
"fmt"
"github.com/sirupsen/logrus"
"gorm.io/gorm/clause"
"gorm.io/gorm/logger"
"gorm.io/gorm/utils"
"os"
@ -317,7 +318,12 @@ func (r *SqlRepo) DeleteInterface(ctx context.Context, id domain.InterfaceIdenti
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 {
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) {
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 {
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 {
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 {
return err
}
@ -486,6 +508,66 @@ func (r *SqlRepo) DeletePeer(ctx context.Context, id domain.PeerIdentifier) erro
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
// region users

View File

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

View File

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

View File

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

View File

@ -13,11 +13,11 @@ type Peer struct {
InterfaceIdentifier string `json:"InterfaceIdentifier"` // the interface id
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
ExpiresAt *time.Time `json:"column:expires_at"` // expiry dates for peers
Notes string `json:"notes"` // a note field for peers
ExpiresAt *time.Time `json:"ExpiresAt"` // expiry dates for peers
Notes string `json:"Notes"` // a note field for peers
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
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
@ -53,7 +53,7 @@ func NewPeer(src *domain.Peer) *Peer {
ExpiresAt: src.ExpiresAt,
Notes: src.Notes,
Endpoint: StringConfigOptionFromDomain(src.Endpoint),
EndpointPublicKey: src.EndpointPublicKey,
EndpointPublicKey: StringConfigOptionFromDomain(src.EndpointPublicKey),
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
PresharedKey: string(src.PresharedKey),
@ -92,7 +92,7 @@ func NewDomainPeer(src *Peer) *domain.Peer {
res := &domain.Peer{
BaseModel: domain.BaseModel{},
Endpoint: StringConfigOptionToDomain(src.Endpoint),
EndpointPublicKey: src.EndpointPublicKey,
EndpointPublicKey: StringConfigOptionToDomain(src.EndpointPublicKey),
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
PresharedKey: domain.PreSharedKey(src.PresharedKey),

View File

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

View File

@ -38,8 +38,10 @@ type WireGuardManager interface {
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) 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)
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 {

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
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUsedIpsPerSubnet(ctx context.Context) (map[domain.Cidr][]domain.Cidr, error)
}
type StatisticsDatabaseRepo interface {

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/h44z/wg-portal/internal/app"
"time"
@ -154,7 +153,7 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
}
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.Interface.Addresses = p.AllowedIPs // use allowed IP's as the peer IP's
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)
}
ipv4, ipv6, err := m.getFreshIpConfig(ctx)
ipv4, ipv6, err := m.getFreshInterfaceIpConfig(ctx)
if err != nil {
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
}
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)
if err != nil {
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)
ipV6, _ = domain.CidrFromString(m.cfg.Advanced.StartCidrV6)
netV4 := ipV4.NetworkAddr()
netV6 := ipV6.NetworkAddr()
for {
ipV4Conflict := false
ipV6Conflict := false
v4Conflict := false
v6Conflict := false
for _, usedIps := range ips {
for _, ip := range usedIps {
if ipV4 == ip {
ipV4Conflict = true
for _, usedIp := range usedIps {
usedNetwork := usedIp.NetworkAddr()
if netV4 == usedNetwork {
v4Conflict = true
}
if ipV6 == ip {
ipV6Conflict = true
if netV6 == usedNetwork {
v6Conflict = true
}
}
}
if !ipV4Conflict && (!useV6 || !ipV6Conflict) {
if !v4Conflict && (!useV6 || !v6Conflict) {
break
}
if ipV4Conflict {
ipV4 = ipV4.NextSubnet()
if v4Conflict {
netV4 = netV4.NextSubnet()
}
if ipV6Conflict && useV6 {
ipV6 = ipV6.NextSubnet()
if v6Conflict && useV6 {
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
}
@ -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)
}
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)
}
@ -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)
}
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)
}
@ -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)
}
if err := m.validateDeletion(ctx, existingInterface); err != nil {
if err := m.validateInterfaceDeletion(ctx, existingInterface); err != nil {
return fmt.Errorf("deletion not allowed: %w", err)
}
@ -553,7 +567,7 @@ func (m Manager) DeleteInterface(ctx context.Context, id domain.InterfaceIdentif
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)
if !currentUser.IsAdmin {
@ -563,7 +577,7 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Int
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)
if new.Identifier == "" {
@ -577,7 +591,7 @@ func (m Manager) validateCreation(ctx context.Context, old, new *domain.Interfac
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)
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)
}
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()
if err != nil {
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
}
peerId := domain.PeerIdentifier(uuid.New().String())
peerId := domain.PeerIdentifier(kp.PublicKey)
freshPeer := &domain.Peer{
BaseModel: domain.BaseModel{
CreatedBy: string(currentUser.Id),
@ -639,7 +658,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
UpdatedAt: time.Now(),
},
Endpoint: domain.NewStringConfigOption(iface.PeerDefEndpoint, true),
EndpointPublicKey: iface.PublicKey,
EndpointPublicKey: domain.NewStringConfigOption(iface.PublicKey, true),
AllowedIPsStr: domain.NewStringConfigOption(iface.PeerDefAllowedIPsStr, true),
ExtraAllowedIPsStr: "",
PresharedKey: pk,
@ -655,7 +674,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
Interface: domain.PeerInterfaceConfig{
KeyPair: kp,
Type: peerMode,
Addresses: nil, // TODO
Addresses: ips,
CheckAliveAddress: "",
DnsStr: domain.NewStringConfigOption(iface.PeerDefDnsStr, true),
DnsSearchStr: domain.NewStringConfigOption(iface.PeerDefDnsSearchStr, true),
@ -672,6 +691,121 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
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 {
peer, err := m.db.GetPeer(ctx, id)
if err != nil {
@ -691,11 +825,36 @@ func (m Manager) DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
return nil
}
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)
func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain.Peer) error {
currentUser := domain.GetUserInfo(ctx)
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()
}
func (c Cidr) IsValid() bool {
return c.Prefix().IsValid()
}
func CidrFromString(str string) (Cidr, error) {
prefix, err := netip.ParsePrefix(strings.TrimSpace(str))
if err != nil {
@ -141,16 +145,20 @@ func (c Cidr) NetworkAddr() Cidr {
func (c Cidr) NextAddr() Cidr {
prefix := c.Prefix()
nextAddr := prefix.Addr().Next()
return Cidr{
Addr: prefix.Addr().Next().String(),
Cidr: netip.PrefixFrom(nextAddr, c.NetLength).String(),
Addr: nextAddr.String(),
NetLength: prefix.Bits(),
}
}
func (c Cidr) NextSubnet() Cidr {
prefix := c.Prefix()
nextAddr := c.BroadcastAddr().Prefix().Addr().Next()
return Cidr{
Addr: c.BroadcastAddr().Prefix().Addr().Next().String(),
Cidr: netip.PrefixFrom(nextAddr, c.NetLength).String(),
Addr: nextAddr.String(),
NetLength: prefix.Bits(),
}
}

View File

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