mirror of
https://github.com/h44z/wg-portal.git
synced 2025-08-12 16:22:23 +00:00
stats display, expiry date field
This commit is contained in:
parent
8e0c59bb52
commit
2235c2a0d7
@ -172,7 +172,7 @@ function handleChangeAllowedIPs(tags) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if(validInput) {
|
if(validInput) {
|
||||||
formData.value.AllowedIPs = tags
|
formData.value.AllowedIPs.Value = tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,10 +340,18 @@ async function del() {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend class="mt-4">State</legend>
|
<legend class="mt-4">State</legend>
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group col-md-6">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
<input class="form-check-input" type="checkbox" v-model="formData.Disabled">
|
||||||
<label class="form-check-label" >Disabled</label>
|
<label class="form-check-label" >Disabled</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-6">
|
||||||
|
<label class="form-label">{{ $t('modals.peeredit.expiresat') }}</label>
|
||||||
|
<input type="date" pattern="\d{4}-\d{2}-\d{2}" class="form-control" min="2023-01-01" v-model="formData.ExpiresAt">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
@ -4,7 +4,7 @@ import {peerStore} from "@/stores/peers";
|
|||||||
import {interfaceStore} from "@/stores/interfaces";
|
import {interfaceStore} from "@/stores/interfaces";
|
||||||
import {computed, ref, watch} from "vue";
|
import {computed, ref, watch} from "vue";
|
||||||
import {useI18n} from "vue-i18n";
|
import {useI18n} from "vue-i18n";
|
||||||
import { freshInterface, freshPeer } from '@/helpers/models';
|
import {freshInterface, freshPeer, freshStats} from '@/helpers/models';
|
||||||
import Prism from "vue-prism-component";
|
import Prism from "vue-prism-component";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
@ -36,6 +36,16 @@ const selectedPeer = computed(() => {
|
|||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedStats = computed(() => {
|
||||||
|
let s = peers.Statistics(props.peerId)
|
||||||
|
|
||||||
|
if (!s) {
|
||||||
|
s = freshStats() // dummy peer to avoid 'undefined' exceptions
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
|
||||||
const selectedInterface = computed(() => {
|
const selectedInterface = computed(() => {
|
||||||
let i = interfaces.GetSelected;
|
let i = interfaces.GetSelected;
|
||||||
|
|
||||||
@ -105,14 +115,14 @@ function email() {
|
|||||||
<template>
|
<template>
|
||||||
<Modal :title="title" :visible="visible" @close="close">
|
<Modal :title="title" :visible="visible" @close="close">
|
||||||
<template #default>
|
<template #default>
|
||||||
<div class="accordion">
|
<div class="accordion" id="peerInformation">
|
||||||
<div class="accordion-item">
|
<div class="accordion-item">
|
||||||
<h2 class="accordion-header">
|
<h2 class="accordion-header">
|
||||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseDetails" aria-expanded="true" aria-controls="collapseDetails">
|
||||||
Peer Information
|
Peer Information
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordionExample" style="">
|
<div id="collapseDetails" class="accordion-collapse collapse show" aria-labelledby="headingDetails" data-bs-parent="#peerInformation" style="">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
@ -122,9 +132,8 @@ function email() {
|
|||||||
<li>IP Addresses: <span v-for="ip in selectedPeer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span></li>
|
<li>IP Addresses: <span v-for="ip in selectedPeer.Addresses" :key="ip" class="badge rounded-pill bg-light">{{ ip }}</span></li>
|
||||||
<li>Linked User: {{ selectedPeer.UserIdentifier }}</li>
|
<li>Linked User: {{ selectedPeer.UserIdentifier }}</li>
|
||||||
<li>Notes: {{ selectedPeer.Notes }}</li>
|
<li>Notes: {{ selectedPeer.Notes }}</li>
|
||||||
|
<li>Expires At: {{ selectedPeer.ExpiresAt }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h4>Traffic</h4>
|
|
||||||
<p><i class="fas fa-long-arrow-alt-down"></i> 1.5 MB / <i class="fas fa-long-arrow-alt-up"></i> 3.9 MB</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<img class="config-qr-img" :src="peers.ConfigQrUrl(props.peerId)" loading="lazy" alt="Configuration QR Code">
|
<img class="config-qr-img" :src="peers.ConfigQrUrl(props.peerId)" loading="lazy" alt="Configuration QR Code">
|
||||||
@ -133,19 +142,42 @@ function email() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header" id="headingStatus">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseStatus" aria-expanded="false" aria-controls="collapseStatus">
|
||||||
|
Current Status
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseStatus" class="accordion-collapse collapse" aria-labelledby="headingStatus" data-bs-parent="#peerInformation" style="">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h4>Traffic</h4>
|
||||||
|
<p><i class="fas fa-long-arrow-alt-down"></i> {{ selectedStats.BytesReceived }} Bytes / <i class="fas fa-long-arrow-alt-up"></i> {{ selectedStats.BytesTransmitted }} Bytes</p>
|
||||||
|
<h4>Connection Stats</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Pingable: {{ selectedStats.IsPingable }}</li>
|
||||||
|
<li>Last Handshake: {{ selectedStats.LastHandshake }}</li>
|
||||||
|
<li>Connected Since: {{ selectedStats.LastSessionStart }}</li>
|
||||||
|
<li>Endpoint: {{ selectedStats.EndpointAddress }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="selectedInterface.Mode==='server'" class="accordion-item">
|
<div v-if="selectedInterface.Mode==='server'" class="accordion-item">
|
||||||
<h2 class="accordion-header" id="headingTwo">
|
<h2 class="accordion-header" id="headingConfig">
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseConfig" aria-expanded="false" aria-controls="collapseConfig">
|
||||||
Peer Configuration
|
Peer Configuration
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#accordionExample" style="">
|
<div id="collapseConfig" class="accordion-collapse collapse" aria-labelledby="headingConfig" data-bs-parent="#peerInformation" style="">
|
||||||
<div class="accordion-body">
|
<div class="accordion-body">
|
||||||
<Prism language="ini" :code="configString"></Prism>
|
<Prism language="ini" :code="configString"></Prism>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
@ -118,3 +118,16 @@ export function freshPeer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function freshStats() {
|
||||||
|
return {
|
||||||
|
IsConnected: false,
|
||||||
|
IsPingable: false,
|
||||||
|
LastHandshake: null,
|
||||||
|
LastPing: null,
|
||||||
|
LastSessionStart: null,
|
||||||
|
BytesTransmitted: 0,
|
||||||
|
BytesReceived: 0,
|
||||||
|
EndpointAddress: ""
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
|||||||
import {apiWrapper} from "../helpers/fetch-wrapper";
|
import {apiWrapper} from "../helpers/fetch-wrapper";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
import {interfaceStore} from "./interfaces";
|
import {interfaceStore} from "./interfaces";
|
||||||
import { freshPeer } from '@/helpers/models';
|
import {freshPeer, freshStats} from '@/helpers/models';
|
||||||
import { base64_url_encode } from '@/helpers/encoding';
|
import { base64_url_encode } from '@/helpers/encoding';
|
||||||
|
|
||||||
const baseUrl = `/peer`
|
const baseUrl = `/peer`
|
||||||
@ -11,6 +11,8 @@ export const peerStore = defineStore({
|
|||||||
id: 'peers',
|
id: 'peers',
|
||||||
state: () => ({
|
state: () => ({
|
||||||
peers: [],
|
peers: [],
|
||||||
|
stats: {},
|
||||||
|
statsEnabled: false,
|
||||||
peer: freshPeer(),
|
peer: freshPeer(),
|
||||||
prepared: freshPeer(),
|
prepared: freshPeer(),
|
||||||
configuration: "",
|
configuration: "",
|
||||||
@ -24,6 +26,7 @@ export const peerStore = defineStore({
|
|||||||
Find: (state) => {
|
Find: (state) => {
|
||||||
return (id) => state.peers.find((p) => p.Identifier === id)
|
return (id) => state.peers.find((p) => p.Identifier === id)
|
||||||
},
|
},
|
||||||
|
|
||||||
Count: (state) => state.peers.length,
|
Count: (state) => state.peers.length,
|
||||||
Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
|
Prepared: (state) => {console.log("STATE:", state.prepared); return state.prepared},
|
||||||
FilteredCount: (state) => state.Filtered.length,
|
FilteredCount: (state) => state.Filtered.length,
|
||||||
@ -46,6 +49,11 @@ export const peerStore = defineStore({
|
|||||||
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
hasNextPage: (state) => state.pageOffset < (state.FilteredCount - state.pageSize),
|
||||||
hasPrevPage: (state) => state.pageOffset > 0,
|
hasPrevPage: (state) => state.pageOffset > 0,
|
||||||
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
currentPage: (state) => (state.pageOffset / state.pageSize)+1,
|
||||||
|
Statistics: (state) => {
|
||||||
|
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||||
|
},
|
||||||
|
hasStatistics: (state) => state.statsEnabled,
|
||||||
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
afterPageSizeChange() {
|
afterPageSizeChange() {
|
||||||
@ -90,6 +98,14 @@ export const peerStore = defineStore({
|
|||||||
setPeerConfig(config) {
|
setPeerConfig(config) {
|
||||||
this.configuration = config;
|
this.configuration = config;
|
||||||
},
|
},
|
||||||
|
setStats(statsResponse) {
|
||||||
|
if (!statsResponse) {
|
||||||
|
this.stats = {}
|
||||||
|
this.statsEnabled = false
|
||||||
|
}
|
||||||
|
this.stats = statsResponse.Stats
|
||||||
|
this.statsEnabled = statsResponse.Enabled
|
||||||
|
},
|
||||||
async PreparePeer(interfaceId) {
|
async PreparePeer(interfaceId) {
|
||||||
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/prepare`)
|
return apiWrapper.get(`${baseUrl}/iface/${base64_url_encode(interfaceId)}/prepare`)
|
||||||
.then(this.setPreparedPeer)
|
.then(this.setPreparedPeer)
|
||||||
@ -143,6 +159,27 @@ export const peerStore = defineStore({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async LoadStats(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/${base64_url_encode(interfaceId)}/stats`)
|
||||||
|
.then(this.setStats)
|
||||||
|
.catch(error => {
|
||||||
|
this.setStats(undefined)
|
||||||
|
console.log("Failed to load peer stats: ", error)
|
||||||
|
notify({
|
||||||
|
title: "Backend Connection Failure",
|
||||||
|
text: "Failed to load peer stats!",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
async DeletePeer(id) {
|
async DeletePeer(id) {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
|
return apiWrapper.delete(`${baseUrl}/${base64_url_encode(id)}`)
|
||||||
|
@ -46,7 +46,8 @@ async function download() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await interfaces.LoadInterfaces()
|
await interfaces.LoadInterfaces()
|
||||||
await peers.LoadPeers()
|
await peers.LoadPeers(undefined) // use default interface
|
||||||
|
await peers.LoadStats(undefined) // use default interface
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -294,7 +295,7 @@ onMounted(async () => {
|
|||||||
<th scope="col">{{ $t('interfaces.tableHeadings[1]') }}</th>
|
<th scope="col">{{ $t('interfaces.tableHeadings[1]') }}</th>
|
||||||
<th scope="col">{{ $t('interfaces.tableHeadings[2]') }}</th>
|
<th scope="col">{{ $t('interfaces.tableHeadings[2]') }}</th>
|
||||||
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.tableHeadings[3]') }}</th>
|
<th v-if="interfaces.GetSelected.Mode==='client'" scope="col">{{ $t('interfaces.tableHeadings[3]') }}</th>
|
||||||
<th scope="col">{{ $t('interfaces.tableHeadings[4]') }}</th>
|
<th v-if="peers.hasStatistics" scope="col">{{ $t('interfaces.tableHeadings[4]') }}</th>
|
||||||
<th scope="col"></th><!-- Actions -->
|
<th scope="col"></th><!-- Actions -->
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -309,7 +310,14 @@ onMounted(async () => {
|
|||||||
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
|
<span v-for="ip in peer.Addresses" :key="ip" class="badge bg-light me-1">{{ ip }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
|
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
|
||||||
<td>{{peer.LastConnected}}</td>
|
<td v-if="peers.hasStatistics">
|
||||||
|
<div v-if="peers.Statistics(peer.Identifier).IsConnected">
|
||||||
|
<span class="badge rounded-pill bg-success"><i class="fa-solid fa-link"></i></span> <span :title="peers.Statistics(peer.Identifier).LastHandshake">Connected</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span class="badge rounded-pill bg-light"><i class="fa-solid fa-link-slash"></i></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="#" title="Show peer" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
<a href="#" title="Show peer" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||||
<a href="#" title="Edit peer" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
<a href="#" title="Edit peer" @click.prevent="editPeerId=peer.Identifier"><i class="fas fa-cog"></i></a>
|
||||||
|
@ -230,6 +230,21 @@ func (r *SqlRepo) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceI
|
|||||||
return in, peers, nil
|
return in, peers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SqlRepo) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats []domain.PeerStatus
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).Where("identifier IN ?", ids).Find(&stats).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
||||||
var interfaces []domain.Interface
|
var interfaces []domain.Interface
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ func (e peerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
|
|||||||
apiGroup := g.Group("/peer", e.authenticator.LoggedIn())
|
apiGroup := g.Group("/peer", e.authenticator.LoggedIn())
|
||||||
|
|
||||||
apiGroup.GET("/iface/:iface/all", e.handleAllGet())
|
apiGroup.GET("/iface/:iface/all", e.handleAllGet())
|
||||||
|
apiGroup.GET("/iface/:iface/stats", e.handleStatsGet())
|
||||||
apiGroup.GET("/iface/:iface/prepare", e.handlePrepareGet())
|
apiGroup.GET("/iface/:iface/prepare", e.handlePrepareGet())
|
||||||
apiGroup.POST("/iface/:iface/new", e.handleCreatePost())
|
apiGroup.POST("/iface/:iface/new", e.handleCreatePost())
|
||||||
apiGroup.POST("/iface/:iface/multiplenew", e.handleCreateMultiplePost())
|
apiGroup.POST("/iface/:iface/multiplenew", e.handleCreateMultiplePost())
|
||||||
@ -404,3 +405,34 @@ func (e peerEndpoint) handleEmailPost() gin.HandlerFunc {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleStatsGet returns a gorm handler function.
|
||||||
|
//
|
||||||
|
// @ID peers_handleStatsGet
|
||||||
|
// @Tags Peer
|
||||||
|
// @Summary Get peer stats for the given interface.
|
||||||
|
// @Produce json
|
||||||
|
// @Param iface path string true "The interface identifier"
|
||||||
|
// @Success 200 {object} model.PeerStats
|
||||||
|
// @Failure 400 {object} model.Error
|
||||||
|
// @Failure 500 {object} model.Error
|
||||||
|
// @Router /peer/iface/{iface}/stats [get]
|
||||||
|
func (e peerEndpoint) handleStatsGet() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ctx := domain.SetUserInfoFromGin(c)
|
||||||
|
|
||||||
|
interfaceId := Base64UrlDecode(c.Param("iface"))
|
||||||
|
if interfaceId == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusBadRequest, Message: "missing iface parameter"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, model.NewPeerStats(true, stats))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,6 +6,38 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const ExpiryDateTimeLayout = "\"2006-01-02\""
|
||||||
|
|
||||||
|
type ExpiryDate struct {
|
||||||
|
*time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON will unmarshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) UnmarshalJSON(b []byte) error {
|
||||||
|
if len(b) == 0 || string(b) == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(ExpiryDateTimeLayout, string(b))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed.IsZero() {
|
||||||
|
d.Time = &parsed
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON will marshal using 2006-01-02 layout
|
||||||
|
func (d *ExpiryDate) MarshalJSON() ([]byte, error) {
|
||||||
|
if d == nil || d.Time == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s := d.Format(ExpiryDateTimeLayout)
|
||||||
|
return []byte(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
type Peer struct {
|
type Peer struct {
|
||||||
Identifier string `json:"Identifier" example:"super_nice_peer"` // peer unique identifier
|
Identifier string `json:"Identifier" example:"super_nice_peer"` // peer unique identifier
|
||||||
DisplayName string `json:"DisplayName"` // a nice display name/ description for the peer
|
DisplayName string `json:"DisplayName"` // a nice display name/ description for the peer
|
||||||
@ -13,7 +45,7 @@ type Peer struct {
|
|||||||
InterfaceIdentifier string `json:"InterfaceIdentifier"` // the interface id
|
InterfaceIdentifier string `json:"InterfaceIdentifier"` // the interface id
|
||||||
Disabled bool `json:"Disabled"` // flag that specifies if the peer is enabled (up) or not (down)
|
Disabled bool `json:"Disabled"` // flag that specifies if the peer is enabled (up) or not (down)
|
||||||
DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled
|
DisabledReason string `json:"DisabledReason"` // the reason why the peer has been disabled
|
||||||
ExpiresAt *time.Time `json:"ExpiresAt"` // expiry dates for peers
|
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty"` // expiry dates for peers
|
||||||
Notes string `json:"Notes"` // a note field for peers
|
Notes string `json:"Notes"` // a note field for peers
|
||||||
|
|
||||||
Endpoint StringConfigOption `json:"Endpoint"` // the endpoint address
|
Endpoint StringConfigOption `json:"Endpoint"` // the endpoint address
|
||||||
@ -50,7 +82,7 @@ func NewPeer(src *domain.Peer) *Peer {
|
|||||||
InterfaceIdentifier: string(src.InterfaceIdentifier),
|
InterfaceIdentifier: string(src.InterfaceIdentifier),
|
||||||
Disabled: src.IsDisabled(),
|
Disabled: src.IsDisabled(),
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
ExpiresAt: src.ExpiresAt,
|
ExpiresAt: ExpiryDate{src.ExpiresAt},
|
||||||
Notes: src.Notes,
|
Notes: src.Notes,
|
||||||
Endpoint: StringConfigOptionFromDomain(src.Endpoint),
|
Endpoint: StringConfigOptionFromDomain(src.Endpoint),
|
||||||
EndpointPublicKey: StringConfigOptionFromDomain(src.EndpointPublicKey),
|
EndpointPublicKey: StringConfigOptionFromDomain(src.EndpointPublicKey),
|
||||||
@ -103,7 +135,7 @@ func NewDomainPeer(src *Peer) *domain.Peer {
|
|||||||
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
|
||||||
Disabled: nil, // set below
|
Disabled: nil, // set below
|
||||||
DisabledReason: src.DisabledReason,
|
DisabledReason: src.DisabledReason,
|
||||||
ExpiresAt: src.ExpiresAt,
|
ExpiresAt: src.ExpiresAt.Time,
|
||||||
Notes: src.Notes,
|
Notes: src.Notes,
|
||||||
Interface: domain.PeerInterfaceConfig{
|
Interface: domain.PeerInterfaceConfig{
|
||||||
KeyPair: domain.KeyPair{
|
KeyPair: domain.KeyPair{
|
||||||
@ -148,3 +180,45 @@ type PeerMailRequest struct {
|
|||||||
Identifiers []string `json:"Identifiers"`
|
Identifiers []string `json:"Identifiers"`
|
||||||
LinkOnly bool `json:"LinkOnly"`
|
LinkOnly bool `json:"LinkOnly"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PeerStats struct {
|
||||||
|
Enabled bool `json:"Enabled" example:"true"` // peer stats tracking enabled
|
||||||
|
|
||||||
|
Stats map[string]PeerStatData `json:"Stats"` // stats, map key = Peer identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPeerStats(enabled bool, src []domain.PeerStatus) *PeerStats {
|
||||||
|
stats := make(map[string]PeerStatData, len(src))
|
||||||
|
|
||||||
|
for _, srcStat := range src {
|
||||||
|
stats[string(srcStat.PeerId)] = PeerStatData{
|
||||||
|
IsConnected: srcStat.IsConnected(),
|
||||||
|
IsPingable: srcStat.IsPingable,
|
||||||
|
LastPing: srcStat.LastPing,
|
||||||
|
BytesReceived: srcStat.BytesReceived,
|
||||||
|
BytesTransmitted: srcStat.BytesTransmitted,
|
||||||
|
LastHandshake: srcStat.LastHandshake,
|
||||||
|
EndpointAddress: srcStat.Endpoint,
|
||||||
|
LastSessionStart: srcStat.LastSessionStart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PeerStats{
|
||||||
|
Enabled: enabled,
|
||||||
|
Stats: stats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PeerStatData struct {
|
||||||
|
IsConnected bool `json:"IsConnected"`
|
||||||
|
|
||||||
|
IsPingable bool `json:"IsPingable"`
|
||||||
|
LastPing *time.Time `json:"LastPing"`
|
||||||
|
|
||||||
|
BytesReceived uint64 `json:"BytesReceived"`
|
||||||
|
BytesTransmitted uint64 `json:"BytesTransmitted"`
|
||||||
|
|
||||||
|
LastHandshake *time.Time `json:"LastHandshake"`
|
||||||
|
EndpointAddress string `json:"EndpointAddress"`
|
||||||
|
LastSessionStart *time.Time `json:"LastSessionStart"`
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ type WireGuardManager interface {
|
|||||||
RestoreInterfaceState(ctx context.Context, updateDbOnError bool, filter ...domain.InterfaceIdentifier) error
|
RestoreInterfaceState(ctx context.Context, updateDbOnError bool, filter ...domain.InterfaceIdentifier) error
|
||||||
CreateDefaultPeer(ctx context.Context, user *domain.User) error
|
CreateDefaultPeer(ctx context.Context, user *domain.User) error
|
||||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error)
|
||||||
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
||||||
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
|
||||||
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
PrepareInterface(ctx context.Context) (*domain.Interface, error)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
type InterfaceAndPeerDatabaseRepo interface {
|
type InterfaceAndPeerDatabaseRepo interface {
|
||||||
GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
|
GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
|
||||||
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||||
|
GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error)
|
||||||
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
GetAllInterfaces(ctx context.Context) ([]domain.Interface, error)
|
||||||
FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error)
|
FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error)
|
||||||
GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error)
|
GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error)
|
||||||
|
@ -137,8 +137,8 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSessionStartTime(oldStats domain.PeerStatus, newReceived, newTransmitted uint64, lastHandshake *time.Time) *time.Time {
|
func getSessionStartTime(oldStats domain.PeerStatus, newReceived, newTransmitted uint64, latestHandshake *time.Time) *time.Time {
|
||||||
if lastHandshake == nil {
|
if latestHandshake == nil {
|
||||||
return nil // currently not connected
|
return nil // currently not connected
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,16 +146,19 @@ func getSessionStartTime(oldStats domain.PeerStatus, newReceived, newTransmitted
|
|||||||
switch {
|
switch {
|
||||||
// old session was never initiated
|
// old session was never initiated
|
||||||
case oldStats.BytesReceived == 0 && oldStats.BytesTransmitted == 0 && (newReceived > 0 || newTransmitted > 0):
|
case oldStats.BytesReceived == 0 && oldStats.BytesTransmitted == 0 && (newReceived > 0 || newTransmitted > 0):
|
||||||
return lastHandshake
|
return latestHandshake
|
||||||
// session never received bytes -> first receive
|
// session never received bytes -> first receive
|
||||||
case oldStats.BytesReceived == 0 && newReceived > 0 && (oldStats.LastHandshake == nil || oldStats.LastHandshake.Before(oldestHandshakeTime)):
|
case oldStats.BytesReceived == 0 && newReceived > 0 && (oldStats.LastHandshake == nil || oldStats.LastHandshake.Before(oldestHandshakeTime)):
|
||||||
return lastHandshake
|
return latestHandshake
|
||||||
// session never transmitted bytes -> first transmit
|
// session never transmitted bytes -> first transmit
|
||||||
case oldStats.BytesTransmitted == 0 && newTransmitted > 0 && (oldStats.LastSessionStart == nil || oldStats.LastHandshake.Before(oldestHandshakeTime)):
|
case oldStats.BytesTransmitted == 0 && newTransmitted > 0 && (oldStats.LastSessionStart == nil || oldStats.LastHandshake.Before(oldestHandshakeTime)):
|
||||||
return lastHandshake
|
return latestHandshake
|
||||||
// session restarted as newer send or transmit counts are lower
|
// session restarted as newer send or transmit counts are lower
|
||||||
case (newReceived != 0 && newReceived < oldStats.BytesReceived) || (newTransmitted != 0 && newTransmitted < oldStats.BytesTransmitted):
|
case (newReceived != 0 && newReceived < oldStats.BytesReceived) || (newTransmitted != 0 && newTransmitted < oldStats.BytesTransmitted):
|
||||||
return lastHandshake
|
return latestHandshake
|
||||||
|
// session initiated (but some bytes were already transmitted
|
||||||
|
case oldStats.LastSessionStart == nil && (newReceived > oldStats.BytesReceived || newTransmitted > oldStats.BytesTransmitted):
|
||||||
|
return latestHandshake
|
||||||
default:
|
default:
|
||||||
return oldStats.LastSessionStart
|
return oldStats.LastSessionStart
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,16 @@ func Test_getSessionStartTime(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: &now,
|
want: &now,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "freshly connected (no prev session but bytes)",
|
||||||
|
args: args{
|
||||||
|
oldStats: domain.PeerStatus{LastSessionStart: nil, BytesReceived: 10, BytesTransmitted: 20},
|
||||||
|
newReceived: 100,
|
||||||
|
newTransmitted: 100,
|
||||||
|
lastHandshake: &now,
|
||||||
|
},
|
||||||
|
want: &now,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "still connected",
|
name: "still connected",
|
||||||
args: args{
|
args: args{
|
||||||
|
@ -269,7 +269,7 @@ func (m Manager) RestoreInterfaceState(ctx context.Context, updateDbOnError bool
|
|||||||
|
|
||||||
func (m Manager) CreateDefaultPeer(ctx context.Context, user *domain.User) error {
|
func (m Manager) CreateDefaultPeer(ctx context.Context, user *domain.User) error {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
return nil
|
return fmt.Errorf("IMPLEMENT ME")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) {
|
func (m Manager) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) {
|
||||||
@ -885,3 +885,17 @@ func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) err
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Manager) GetPeerStats(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.PeerStatus, error) {
|
||||||
|
_, peers, err := m.db.GetInterfaceAndPeers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch peers for interface %s: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerIds := make([]domain.PeerIdentifier, len(peers))
|
||||||
|
for i, peer := range peers {
|
||||||
|
peerIds[i] = peer.Identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.db.GetPeersStats(ctx, peerIds...)
|
||||||
|
}
|
||||||
|
@ -132,10 +132,10 @@ type PhysicalPeer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
func (p PhysicalPeer) GetPresharedKey() *wgtypes.Key {
|
||||||
if p.PrivateKey == "" {
|
if p.PresharedKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
key, err := wgtypes.ParseKey(p.PrivateKey)
|
key, err := wgtypes.ParseKey(string(p.PresharedKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,17 @@ type PeerStatus struct {
|
|||||||
LastSessionStart *time.Time `gorm:"column:last_session_start"`
|
LastSessionStart *time.Time `gorm:"column:last_session_start"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s PeerStatus) IsConnected() bool {
|
||||||
|
oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
|
||||||
|
|
||||||
|
handshakeValid := false
|
||||||
|
if s.LastHandshake != nil {
|
||||||
|
handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.IsPingable || handshakeValid
|
||||||
|
}
|
||||||
|
|
||||||
type InterfaceStatus struct {
|
type InterfaceStatus struct {
|
||||||
InterfaceId InterfaceIdentifier `gorm:"primaryKey;column:identifier"`
|
InterfaceId InterfaceIdentifier `gorm:"primaryKey;column:identifier"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user