diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go
index f62d675..39e6d08 100644
--- a/cmd/wg-portal/main.go
+++ b/cmd/wg-portal/main.go
@@ -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)
diff --git a/frontend/src/components/InterfaceEditModal.vue b/frontend/src/components/InterfaceEditModal.vue
index 2a06b04..a1628c5 100644
--- a/frontend/src/components/InterfaceEditModal.vue
+++ b/frontend/src/components/InterfaceEditModal.vue
@@ -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!",
diff --git a/frontend/src/components/PeerEditModal.vue b/frontend/src/components/PeerEditModal.vue
index a1fb194..04b8d13 100644
--- a/frontend/src/components/PeerEditModal.vue
+++ b/frontend/src/components/PeerEditModal.vue
@@ -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',
+ })
+ }
+}
+
@@ -188,6 +279,10 @@ function close() {
+
+
+
+
@@ -228,18 +347,14 @@ function close() {
-
-
-
-
-
+
-
-
+
+
diff --git a/frontend/src/helpers/models.js b/frontend/src/helpers/models.js
new file mode 100644
index 0000000..feb6ce5
--- /dev/null
+++ b/frontend/src/helpers/models.js
@@ -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,
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/helpers/validators.js b/frontend/src/helpers/validators.js
new file mode 100644
index 0000000..90766ba
--- /dev/null
+++ b/frontend/src/helpers/validators.js
@@ -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
+}
\ No newline at end of file
diff --git a/frontend/src/stores/interfaces.js b/frontend/src/stores/interfaces.js
index 38921fd..f09cc22 100644
--- a/frontend/src/stores/interfaces.js
+++ b/frontend/src/stores/interfaces.js
@@ -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
diff --git a/frontend/src/stores/peers.js b/frontend/src/stores/peers.js
index 01de482..8461536 100644
--- a/frontend/src/stores/peers.js
+++ b/frontend/src/stores/peers.js
@@ -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([])
diff --git a/frontend/src/stores/profile.js b/frontend/src/stores/profile.js
index 4708824..cfef6b8 100644
--- a/frontend/src/stores/profile.js
+++ b/frontend/src/stores/profile.js
@@ -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({})
diff --git a/frontend/src/stores/users.js b/frontend/src/stores/users.js
index 5d1516d..87e3c85 100644
--- a/frontend/src/stores/users.js
+++ b/frontend/src/stores/users.js
@@ -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([])
diff --git a/frontend/src/views/InterfaceView.vue b/frontend/src/views/InterfaceView.vue
index a76a96c..5ea3b92 100644
--- a/frontend/src/views/InterfaceView.vue
+++ b/frontend/src/views/InterfaceView.vue
@@ -289,7 +289,7 @@ onMounted(async () => {
{{ ip }}
|
- {{peer.Endpoint}} |
+ {{peer.Endpoint.Value}} |
{{peer.LastConnected}} |
diff --git a/internal/adapters/database.go b/internal/adapters/database.go
index 838abab..d44ec73 100644
--- a/internal/adapters/database.go
+++ b/internal/adapters/database.go
@@ -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
diff --git a/internal/app/api/v0/handlers/endpoint_peers.go b/internal/app/api/v0/handlers/endpoint_peers.go
index 57a6ef0..62764bc 100644
--- a/internal/app/api/v0/handlers/endpoint_peers.go
+++ b/internal/app/api/v0/handlers/endpoint_peers.go
@@ -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
}
diff --git a/internal/app/api/v0/model/model_options.go b/internal/app/api/v0/model/model_options.go
index b40e9db..b0be72f 100644
--- a/internal/app/api/v0/model/model_options.go
+++ b/internal/app/api/v0/model/model_options.go
@@ -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 {
diff --git a/internal/app/api/v0/model/models.go b/internal/app/api/v0/model/models.go
index 40f2491..95c9cfc 100644
--- a/internal/app/api/v0/model/models.go
+++ b/internal/app/api/v0/model/models.go
@@ -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"`
}
diff --git a/internal/app/api/v0/model/models_peer.go b/internal/app/api/v0/model/models_peer.go
index fdd4a40..aa9f46f 100644
--- a/internal/app/api/v0/model/models_peer.go
+++ b/internal/app/api/v0/model/models_peer.go
@@ -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),
diff --git a/internal/app/migrate_v1.go b/internal/app/migrate_v1.go
index d5308c7..ccb3483 100644
--- a/internal/app/migrate_v1.go
+++ b/internal/app/migrate_v1.go
@@ -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,
diff --git a/internal/app/repos.go b/internal/app/repos.go
index 22d0876..d8b8aee 100644
--- a/internal/app/repos.go
+++ b/internal/app/repos.go
@@ -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 {
diff --git a/internal/app/wireguard/repos.go b/internal/app/wireguard/repos.go
index 15a0b1e..7d6ffd8 100644
--- a/internal/app/wireguard/repos.go
+++ b/internal/app/wireguard/repos.go
@@ -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 {
diff --git a/internal/app/wireguard/wireguard.go b/internal/app/wireguard/wireguard.go
index 5efb484..9ce46fe 100644
--- a/internal/app/wireguard/wireguard.go
+++ b/internal/app/wireguard/wireguard.go
@@ -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
}
diff --git a/internal/domain/ip.go b/internal/domain/ip.go
index 47b664a..3ec1367 100644
--- a/internal/domain/ip.go
+++ b/internal/domain/ip.go
@@ -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(),
}
}
diff --git a/internal/domain/peer.go b/internal/domain/peer.go
index 52cd70c..f5b3ffd 100644
--- a/internal/domain/peer.go
+++ b/internal/domain/peer.go
@@ -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,
|