mirror of
https://github.com/h44z/wg-portal.git
synced 2025-08-09 23:12:24 +00:00
peer creation and deletion
This commit is contained in:
parent
e38c48bede
commit
4a53a5207a
@ -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)
|
||||
|
@ -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!",
|
||||
|
@ -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>
|
||||
|
120
frontend/src/helpers/models.js
Normal file
120
frontend/src/helpers/models.js
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
14
frontend/src/helpers/validators.js
Normal file
14
frontend/src/helpers/validators.js
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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([])
|
||||
|
@ -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({})
|
||||
|
@ -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([])
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user