mirror of
https://github.com/h44z/wg-portal.git
synced 2026-02-23 10:56:22 +00:00
Compare commits
2 Commits
dependabot
...
multi_auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79eaedb9ca | ||
|
|
70832bfb52 |
6
.github/workflows/chart.yml
vendored
6
.github/workflows/chart.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
# ct lint requires Python 3.x to run following packages:
|
# ct lint requires Python 3.x to run following packages:
|
||||||
# - yamale (https://github.com/23andMe/Yamale)
|
# - yamale (https://github.com/23andMe/Yamale)
|
||||||
# - yamllint (https://github.com/adrienverge/yamllint)
|
# - yamllint (https://github.com/adrienverge/yamllint)
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.x'
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|||||||
4
.github/workflows/pages.yml
vendored
4
.github/workflows/pages.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ RUN npm run build
|
|||||||
######
|
######
|
||||||
# Build backend
|
# Build backend
|
||||||
######
|
######
|
||||||
FROM --platform=${BUILDPLATFORM} golang:1.26-alpine AS builder
|
FROM --platform=${BUILDPLATFORM} golang:1.25-alpine AS builder
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
# Download dependencies
|
# Download dependencies
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ func main() {
|
|||||||
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
apiV0EndpointPeers := handlersV0.NewPeerEndpoint(cfg, apiV0Auth, validatorManager, apiV0BackendPeers)
|
||||||
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
|
apiV0EndpointConfig := handlersV0.NewConfigEndpoint(cfg, apiV0Auth, wireGuard)
|
||||||
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
apiV0EndpointTest := handlersV0.NewTestEndpoint(apiV0Auth)
|
||||||
apiV0EndpointWebsocket := handlersV0.NewWebsocketEndpoint(cfg, apiV0Auth, eventBus)
|
|
||||||
|
|
||||||
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
apiFrontend := handlersV0.NewRestApi(apiV0Session,
|
||||||
apiV0EndpointAuth,
|
apiV0EndpointAuth,
|
||||||
@@ -145,7 +144,6 @@ func main() {
|
|||||||
apiV0EndpointPeers,
|
apiV0EndpointPeers,
|
||||||
apiV0EndpointConfig,
|
apiV0EndpointConfig,
|
||||||
apiV0EndpointTest,
|
apiV0EndpointTest,
|
||||||
apiV0EndpointWebsocket,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// endregion API v0 (SPA frontend)
|
// endregion API v0 (SPA frontend)
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { peerStore } from '@/stores/peers';
|
|
||||||
import { interfaceStore } from '@/stores/interfaces';
|
|
||||||
import { authStore } from '@/stores/auth';
|
|
||||||
|
|
||||||
let socket = null;
|
|
||||||
let reconnectTimer = null;
|
|
||||||
let failureCount = 0;
|
|
||||||
|
|
||||||
export const websocketWrapper = {
|
|
||||||
connect() {
|
|
||||||
if (socket) {
|
|
||||||
console.log('WebSocket already connected, re-using existing connection.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const protocol = WGPORTAL_BACKEND_BASE_URL.startsWith('https://') ? 'wss://' : 'ws://';
|
|
||||||
const baseUrl = WGPORTAL_BACKEND_BASE_URL.replace(/^https?:\/\//, '');
|
|
||||||
const url = `${protocol}${baseUrl}/ws`;
|
|
||||||
|
|
||||||
socket = new WebSocket(url);
|
|
||||||
|
|
||||||
socket.onopen = () => {
|
|
||||||
console.log('WebSocket connected');
|
|
||||||
failureCount = 0;
|
|
||||||
if (reconnectTimer) {
|
|
||||||
clearInterval(reconnectTimer);
|
|
||||||
reconnectTimer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onclose = () => {
|
|
||||||
console.log('WebSocket disconnected');
|
|
||||||
failureCount++;
|
|
||||||
socket = null;
|
|
||||||
this.scheduleReconnect();
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onerror = (error) => {
|
|
||||||
console.error('WebSocket error:', error);
|
|
||||||
failureCount++;
|
|
||||||
socket.close();
|
|
||||||
socket = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.onmessage = (event) => {
|
|
||||||
const message = JSON.parse(event.data);
|
|
||||||
switch (message.type) {
|
|
||||||
case 'peer_stats':
|
|
||||||
peerStore().updatePeerTrafficStats(message.data);
|
|
||||||
break;
|
|
||||||
case 'interface_stats':
|
|
||||||
interfaceStore().updateInterfaceTrafficStats(message.data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
if (socket) {
|
|
||||||
socket.close();
|
|
||||||
socket = null;
|
|
||||||
}
|
|
||||||
if (reconnectTimer) {
|
|
||||||
clearInterval(reconnectTimer);
|
|
||||||
reconnectTimer = null;
|
|
||||||
failureCount = 0;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
scheduleReconnect() {
|
|
||||||
if (reconnectTimer) return;
|
|
||||||
if (!authStore().IsAuthenticated) return; // Don't reconnect if not logged in
|
|
||||||
|
|
||||||
reconnectTimer = setInterval(() => {
|
|
||||||
if (failureCount > 2) {
|
|
||||||
console.log('WebSocket connection unavailable, giving up.');
|
|
||||||
clearInterval(reconnectTimer);
|
|
||||||
reconnectTimer = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Attempting to reconnect WebSocket...');
|
|
||||||
this.connect();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -2,7 +2,6 @@ import { defineStore } from 'pinia'
|
|||||||
|
|
||||||
import { notify } from "@kyvg/vue3-notification";
|
import { notify } from "@kyvg/vue3-notification";
|
||||||
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
import { apiWrapper } from '@/helpers/fetch-wrapper'
|
||||||
import { websocketWrapper } from '@/helpers/websocket-wrapper'
|
|
||||||
import router from '../router'
|
import router from '../router'
|
||||||
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
|
import { browserSupportsWebAuthn,startRegistration,startAuthentication } from '@simplewebauthn/browser';
|
||||||
import {base64_url_encode} from "@/helpers/encoding";
|
import {base64_url_encode} from "@/helpers/encoding";
|
||||||
@@ -296,11 +295,9 @@ export const authStore = defineStore('auth',{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
localStorage.setItem('user', JSON.stringify(this.user))
|
localStorage.setItem('user', JSON.stringify(this.user))
|
||||||
websocketWrapper.connect()
|
|
||||||
} else {
|
} else {
|
||||||
this.user = null
|
this.user = null
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem('user')
|
||||||
websocketWrapper.disconnect()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setWebAuthnCredentials(credentials) {
|
setWebAuthnCredentials(credentials) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const interfaceStore = defineStore('interfaces', {
|
|||||||
configuration: "",
|
configuration: "",
|
||||||
selected: "",
|
selected: "",
|
||||||
fetching: false,
|
fetching: false,
|
||||||
trafficStats: {},
|
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
Count: (state) => state.interfaces.length,
|
Count: (state) => state.interfaces.length,
|
||||||
@@ -25,9 +24,6 @@ export const interfaceStore = defineStore('interfaces', {
|
|||||||
},
|
},
|
||||||
GetSelected: (state) => state.interfaces.find((i) => i.Identifier === state.selected) || state.interfaces[0],
|
GetSelected: (state) => state.interfaces.find((i) => i.Identifier === state.selected) || state.interfaces[0],
|
||||||
isFetching: (state) => state.fetching,
|
isFetching: (state) => state.fetching,
|
||||||
TrafficStats: (state) => {
|
|
||||||
return (state.selected in state.trafficStats) ? state.trafficStats[state.selected] : { Received: 0, Transmitted: 0 }
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setInterfaces(interfaces) {
|
setInterfaces(interfaces) {
|
||||||
@@ -38,14 +34,6 @@ export const interfaceStore = defineStore('interfaces', {
|
|||||||
this.selected = ""
|
this.selected = ""
|
||||||
}
|
}
|
||||||
this.fetching = false
|
this.fetching = false
|
||||||
this.trafficStats = {}
|
|
||||||
},
|
|
||||||
updateInterfaceTrafficStats(interfaceStats) {
|
|
||||||
const id = interfaceStats.EntityId;
|
|
||||||
this.trafficStats[id] = {
|
|
||||||
Received: interfaceStats.BytesReceived,
|
|
||||||
Transmitted: interfaceStats.BytesTransmitted,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
async LoadInterfaces() {
|
async LoadInterfaces() {
|
||||||
this.fetching = true
|
this.fetching = true
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const peerStore = defineStore('peers', {
|
|||||||
fetching: false,
|
fetching: false,
|
||||||
sortKey: 'IsConnected', // Default sort key
|
sortKey: 'IsConnected', // Default sort key
|
||||||
sortOrder: -1, // 1 for ascending, -1 for descending
|
sortOrder: -1, // 1 for ascending, -1 for descending
|
||||||
trafficStats: {},
|
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
Find: (state) => {
|
Find: (state) => {
|
||||||
@@ -77,9 +76,6 @@ export const peerStore = defineStore('peers', {
|
|||||||
Statistics: (state) => {
|
Statistics: (state) => {
|
||||||
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
return (id) => state.statsEnabled && (id in state.stats) ? state.stats[id] : freshStats()
|
||||||
},
|
},
|
||||||
TrafficStats: (state) => {
|
|
||||||
return (id) => (id in state.trafficStats) ? state.trafficStats[id] : { Received: 0, Transmitted: 0 }
|
|
||||||
},
|
|
||||||
hasStatistics: (state) => state.statsEnabled,
|
hasStatistics: (state) => state.statsEnabled,
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -115,7 +111,6 @@ export const peerStore = defineStore('peers', {
|
|||||||
this.peers = peers
|
this.peers = peers
|
||||||
this.calculatePages()
|
this.calculatePages()
|
||||||
this.fetching = false
|
this.fetching = false
|
||||||
this.trafficStats = {}
|
|
||||||
},
|
},
|
||||||
setPeer(peer) {
|
setPeer(peer) {
|
||||||
this.peer = peer
|
this.peer = peer
|
||||||
@@ -131,19 +126,11 @@ export const peerStore = defineStore('peers', {
|
|||||||
if (!statsResponse) {
|
if (!statsResponse) {
|
||||||
this.stats = {}
|
this.stats = {}
|
||||||
this.statsEnabled = false
|
this.statsEnabled = false
|
||||||
this.trafficStats = {}
|
|
||||||
} else {
|
} else {
|
||||||
this.stats = statsResponse.Stats
|
this.stats = statsResponse.Stats
|
||||||
this.statsEnabled = statsResponse.Enabled
|
this.statsEnabled = statsResponse.Enabled
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePeerTrafficStats(peerStats) {
|
|
||||||
const id = peerStats.EntityId;
|
|
||||||
this.trafficStats[id] = {
|
|
||||||
Received: peerStats.BytesReceived,
|
|
||||||
Transmitted: peerStats.BytesTransmitted,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async Reset() {
|
async Reset() {
|
||||||
this.setPeers([])
|
this.setPeers([])
|
||||||
this.setStats(undefined)
|
this.setStats(undefined)
|
||||||
|
|||||||
@@ -210,12 +210,6 @@ onMounted(async () => {
|
|||||||
<div class="col-12 col-lg-8">
|
<div class="col-12 col-lg-8">
|
||||||
{{ $t('interfaces.interface.headline') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{ $t('modals.interface-edit.mode.' + interfaces.GetSelected.Mode )}} | {{ $t('interfaces.interface.backend') + ": " + calculateBackendName }}<span v-if="!isBackendValid" :title="t('interfaces.interface.wrong-backend')" class="ms-1 me-1"><i class="fa-solid fa-triangle-exclamation"></i></span>)
|
{{ $t('interfaces.interface.headline') }} <strong>{{interfaces.GetSelected.Identifier}}</strong> ({{ $t('modals.interface-edit.mode.' + interfaces.GetSelected.Mode )}} | {{ $t('interfaces.interface.backend') + ": " + calculateBackendName }}<span v-if="!isBackendValid" :title="t('interfaces.interface.wrong-backend')" class="ms-1 me-1"><i class="fa-solid fa-triangle-exclamation"></i></span>)
|
||||||
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
<span v-if="interfaces.GetSelected.Disabled" class="text-danger"><i class="fa fa-circle-xmark" :title="interfaces.GetSelected.DisabledReason"></i></span>
|
||||||
<div v-if="interfaces.GetSelected && (interfaces.TrafficStats.Received > 0 || interfaces.TrafficStats.Transmitted > 0)" class="mt-2">
|
|
||||||
<small class="text-muted">
|
|
||||||
Traffic: <i class="fa-solid fa-arrow-down me-1"></i>{{ humanFileSize(interfaces.TrafficStats.Received) }}/s
|
|
||||||
<i class="fa-solid fa-arrow-up ms-1 me-1"></i>{{ humanFileSize(interfaces.TrafficStats.Transmitted) }}/s
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4 text-lg-end">
|
<div class="col-12 col-lg-4 text-lg-end">
|
||||||
<a class="btn-link" href="#" :title="$t('interfaces.interface.button-show-config')" @click.prevent="viewedInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-eye"></i></a>
|
<a class="btn-link" href="#" :title="$t('interfaces.interface.button-show-config')" @click.prevent="viewedInterfaceId=interfaces.GetSelected.Identifier"><i class="fas fa-eye"></i></a>
|
||||||
@@ -457,19 +451,14 @@ onMounted(async () => {
|
|||||||
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
|
<td v-if="interfaces.GetSelected.Mode==='client'">{{peer.Endpoint.Value}}</td>
|
||||||
<td v-if="peers.hasStatistics">
|
<td v-if="peers.hasStatistics">
|
||||||
<div v-if="peers.Statistics(peer.Identifier).IsConnected">
|
<div v-if="peers.Statistics(peer.Identifier).IsConnected">
|
||||||
<span class="badge rounded-pill bg-success" :title="$t('interfaces.peer-connected')"><i class="fa-solid fa-link"></i></span> <small class="text-muted" :title="$t('interfaces.peer-handshake') + ' ' + peers.Statistics(peer.Identifier).LastHandshake"><i class="fa-solid fa-circle-info"></i></small>
|
<span class="badge rounded-pill bg-success" :title="$t('interfaces.peer-connected')"><i class="fa-solid fa-link"></i></span> <span :title="$t('interfaces.peer-handshake') + ' ' + peers.Statistics(peer.Identifier).LastHandshake">{{ $t('interfaces.peer-connected') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span class="badge rounded-pill bg-light" :title="$t('interfaces.peer-not-connected')"><i class="fa-solid fa-link-slash"></i></span>
|
<span class="badge rounded-pill bg-light" :title="$t('interfaces.peer-not-connected')"><i class="fa-solid fa-link-slash"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="peers.hasStatistics" >
|
<td v-if="peers.hasStatistics" >
|
||||||
<div class="d-flex flex-column">
|
<span class="text-center" >{{ humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) }} / {{ humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted) }}</span>
|
||||||
<span :title="humanFileSize(peers.Statistics(peer.Identifier).BytesReceived) + ' / ' + humanFileSize(peers.Statistics(peer.Identifier).BytesTransmitted)">
|
|
||||||
<i class="fa-solid fa-arrow-down me-1"></i>{{ humanFileSize(peers.TrafficStats(peer.Identifier).Received) }}/s
|
|
||||||
<i class="fa-solid fa-arrow-up ms-1 me-1"></i>{{ humanFileSize(peers.TrafficStats(peer.Identifier).Transmitted) }}/s
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
<a href="#" :title="$t('interfaces.button-show-peer')" @click.prevent="viewedPeerId=peer.Identifier"><i class="fas fa-eye me-2"></i></a>
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -12,7 +12,6 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.30.1
|
github.com/go-playground/validator/v10 v10.30.1
|
||||||
github.com/go-webauthn/webauthn v0.15.0
|
github.com/go-webauthn/webauthn v0.15.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
|
||||||
github.com/prometheus-community/pro-bing v0.7.0
|
github.com/prometheus-community/pro-bing v0.7.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
@@ -22,9 +21,9 @@ require (
|
|||||||
github.com/xhit/go-simple-mail/v2 v2.16.0
|
github.com/xhit/go-simple-mail/v2 v2.16.0
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
github.com/yeqown/go-qrcode/writer/compressed v1.0.1
|
||||||
golang.org/x/crypto v0.47.0
|
golang.org/x/crypto v0.46.0
|
||||||
golang.org/x/oauth2 v0.34.0
|
golang.org/x/oauth2 v0.34.0
|
||||||
golang.org/x/sys v0.40.0
|
golang.org/x/sys v0.39.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.6.0
|
gorm.io/driver/mysql v1.6.0
|
||||||
@@ -97,7 +96,7 @@ require (
|
|||||||
golang.org/x/mod v0.31.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.40.0 // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
|||||||
14
go.sum
14
go.sum
@@ -130,8 +130,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
@@ -272,8 +270,8 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
|
|||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
@@ -334,8 +332,8 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -364,8 +362,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ func (c LocalController) exec(command string, interfaceId domain.InterfaceIdenti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to executed shell command",
|
slog.Warn("failed to executed shell command",
|
||||||
"command", commandWithInterfaceName, "stdin", stdin, "output", string(out), "error", err)
|
"command", commandWithInterfaceName, "stdin", stdin, "output", string(out), "error", err)
|
||||||
return fmt.Errorf("failed to execute shell command %s: %w", commandWithInterfaceName, err)
|
return fmt.Errorf("failed to exexute shell command %s: %w", commandWithInterfaceName, err)
|
||||||
}
|
}
|
||||||
slog.Debug("executed shell command",
|
slog.Debug("executed shell command",
|
||||||
"command", commandWithInterfaceName,
|
"command", commandWithInterfaceName,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,12 +38,6 @@ func (w *writerWrapper) Write(data []byte) (int, error) {
|
|||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijack wraps the Hijack method of the ResponseWriter and returns the hijacked connection.
|
|
||||||
// This is required for websockets to work.
|
|
||||||
func (w *writerWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
||||||
return http.NewResponseController(w.ResponseWriter).Hijack()
|
|
||||||
}
|
|
||||||
|
|
||||||
// newWriterWrapper returns a new writerWrapper that wraps the given http.ResponseWriter.
|
// newWriterWrapper returns a new writerWrapper that wraps the given http.ResponseWriter.
|
||||||
// It initializes the StatusCode to http.StatusOK.
|
// It initializes the StatusCode to http.StatusOK.
|
||||||
func newWriterWrapper(w http.ResponseWriter) *writerWrapper {
|
func newWriterWrapper(w http.ResponseWriter) *writerWrapper {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-pkgz/routegroup"
|
"github.com/go-pkgz/routegroup"
|
||||||
@@ -448,17 +449,7 @@ func (e AuthEndpoint) handleLogoutPost() http.HandlerFunc {
|
|||||||
|
|
||||||
// isValidReturnUrl checks if the given return URL matches the configured external URL of the application.
|
// isValidReturnUrl checks if the given return URL matches the configured external URL of the application.
|
||||||
func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
|
func (e AuthEndpoint) isValidReturnUrl(returnUrl string) bool {
|
||||||
expectedUrl, err := url.Parse(e.cfg.Web.ExternalUrl)
|
if !strings.HasPrefix(returnUrl, e.cfg.Web.ExternalUrl) {
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
returnUrlParsed, err := url.Parse(returnUrl)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if returnUrlParsed.Scheme != expectedUrl.Scheme || returnUrlParsed.Host != expectedUrl.Host {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/go-pkgz/routegroup"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/app"
|
|
||||||
"github.com/h44z/wg-portal/internal/config"
|
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WebsocketEventBus interface {
|
|
||||||
Subscribe(topic string, fn any) error
|
|
||||||
Unsubscribe(topic string, fn any) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketEndpoint struct {
|
|
||||||
authenticator Authenticator
|
|
||||||
bus WebsocketEventBus
|
|
||||||
|
|
||||||
upgrader websocket.Upgrader
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWebsocketEndpoint(cfg *config.Config, auth Authenticator, bus WebsocketEventBus) *WebsocketEndpoint {
|
|
||||||
return &WebsocketEndpoint{
|
|
||||||
authenticator: auth,
|
|
||||||
bus: bus,
|
|
||||||
upgrader: websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
origin := r.Header.Get("Origin")
|
|
||||||
return strings.HasPrefix(origin, cfg.Web.ExternalUrl)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e WebsocketEndpoint) GetName() string {
|
|
||||||
return "WebsocketEndpoint"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e WebsocketEndpoint) RegisterRoutes(g *routegroup.Bundle) {
|
|
||||||
g.With(e.authenticator.LoggedIn()).HandleFunc("GET /ws", e.handleWebsocket())
|
|
||||||
}
|
|
||||||
|
|
||||||
// wsMessage represents a message sent over websocket to the frontend
|
|
||||||
type wsMessage struct {
|
|
||||||
Type string `json:"type"` // either "peer_stats" or "interface_stats"
|
|
||||||
Data any `json:"data"` // domain.TrafficDelta
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e WebsocketEndpoint) handleWebsocket() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
conn, err := e.upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(r.Context())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
writeMutex := sync.Mutex{}
|
|
||||||
writeJSON := func(msg wsMessage) error {
|
|
||||||
writeMutex.Lock()
|
|
||||||
defer writeMutex.Unlock()
|
|
||||||
return conn.WriteJSON(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
peerStatsHandler := func(status domain.TrafficDelta) {
|
|
||||||
_ = writeJSON(wsMessage{Type: "peer_stats", Data: status})
|
|
||||||
}
|
|
||||||
interfaceStatsHandler := func(status domain.TrafficDelta) {
|
|
||||||
_ = writeJSON(wsMessage{Type: "interface_stats", Data: status})
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = e.bus.Subscribe(app.TopicPeerStatsUpdated, peerStatsHandler)
|
|
||||||
defer e.bus.Unsubscribe(app.TopicPeerStatsUpdated, peerStatsHandler)
|
|
||||||
_ = e.bus.Subscribe(app.TopicInterfaceStatsUpdated, interfaceStatsHandler)
|
|
||||||
defer e.bus.Unsubscribe(app.TopicInterfaceStatsUpdated, interfaceStatsHandler)
|
|
||||||
|
|
||||||
// Keep connection open until client disconnects or context is cancelled
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
if _, _, err := conn.ReadMessage(); err != nil {
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-ctx.Done()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,7 +26,6 @@ const TopicUserEnabled = "user:enabled"
|
|||||||
const TopicInterfaceCreated = "interface:created"
|
const TopicInterfaceCreated = "interface:created"
|
||||||
const TopicInterfaceUpdated = "interface:updated"
|
const TopicInterfaceUpdated = "interface:updated"
|
||||||
const TopicInterfaceDeleted = "interface:deleted"
|
const TopicInterfaceDeleted = "interface:deleted"
|
||||||
const TopicInterfaceStatsUpdated = "interface:stats:updated"
|
|
||||||
|
|
||||||
// endregion interface-events
|
// endregion interface-events
|
||||||
|
|
||||||
@@ -38,7 +37,6 @@ const TopicPeerUpdated = "peer:updated"
|
|||||||
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
const TopicPeerInterfaceUpdated = "peer:interface:updated"
|
||||||
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
const TopicPeerIdentifierUpdated = "peer:identifier:updated"
|
||||||
const TopicPeerStateChanged = "peer:state:changed"
|
const TopicPeerStateChanged = "peer:state:changed"
|
||||||
const TopicPeerStatsUpdated = "peer:stats:updated"
|
|
||||||
|
|
||||||
// endregion peer-events
|
// endregion peer-events
|
||||||
|
|
||||||
|
|||||||
@@ -121,25 +121,15 @@ func (c *StatisticsCollector) collectInterfaceData(ctx context.Context) {
|
|||||||
"error", err)
|
"error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
err = c.db.UpdateInterfaceStatus(ctx, in.Identifier,
|
err = c.db.UpdateInterfaceStatus(ctx, in.Identifier,
|
||||||
func(i *domain.InterfaceStatus) (*domain.InterfaceStatus, error) {
|
func(i *domain.InterfaceStatus) (*domain.InterfaceStatus, error) {
|
||||||
td := domain.CalculateTrafficDelta(
|
i.UpdatedAt = time.Now()
|
||||||
string(in.Identifier),
|
|
||||||
i.UpdatedAt, now,
|
|
||||||
i.BytesTransmitted, physicalInterface.BytesUpload,
|
|
||||||
i.BytesReceived, physicalInterface.BytesDownload,
|
|
||||||
)
|
|
||||||
i.UpdatedAt = now
|
|
||||||
i.BytesReceived = physicalInterface.BytesDownload
|
i.BytesReceived = physicalInterface.BytesDownload
|
||||||
i.BytesTransmitted = physicalInterface.BytesUpload
|
i.BytesTransmitted = physicalInterface.BytesUpload
|
||||||
|
|
||||||
// Update prometheus metrics
|
// Update prometheus metrics
|
||||||
go c.updateInterfaceMetrics(*i)
|
go c.updateInterfaceMetrics(*i)
|
||||||
|
|
||||||
// Publish stats update event
|
|
||||||
c.bus.Publish(app.TopicInterfaceStatsUpdated, td)
|
|
||||||
|
|
||||||
return i, nil
|
return i, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -182,7 +172,6 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
slog.Warn("failed to fetch peers for data collection", "interface", in.Identifier, "error", err)
|
slog.Warn("failed to fetch peers for data collection", "interface", in.Identifier, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
for _, peer := range peers {
|
for _, peer := range peers {
|
||||||
var connectionStateChanged bool
|
var connectionStateChanged bool
|
||||||
var newPeerStatus domain.PeerStatus
|
var newPeerStatus domain.PeerStatus
|
||||||
@@ -195,15 +184,8 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
lastHandshake = &peer.LastHandshake
|
lastHandshake = &peer.LastHandshake
|
||||||
}
|
}
|
||||||
|
|
||||||
td := domain.CalculateTrafficDelta(
|
|
||||||
string(peer.Identifier),
|
|
||||||
p.UpdatedAt, now,
|
|
||||||
p.BytesTransmitted, peer.BytesDownload,
|
|
||||||
p.BytesReceived, peer.BytesUpload,
|
|
||||||
)
|
|
||||||
|
|
||||||
// calculate if session was restarted
|
// calculate if session was restarted
|
||||||
p.UpdatedAt = now
|
p.UpdatedAt = time.Now()
|
||||||
p.LastSessionStart = getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
|
p.LastSessionStart = getSessionStartTime(*p, peer.BytesUpload, peer.BytesDownload,
|
||||||
lastHandshake)
|
lastHandshake)
|
||||||
p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server
|
p.BytesReceived = peer.BytesUpload // store bytes that where uploaded from the peer and received by the server
|
||||||
@@ -213,8 +195,7 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
p.CalcConnected()
|
p.CalcConnected()
|
||||||
|
|
||||||
if wasConnected != p.IsConnected {
|
if wasConnected != p.IsConnected {
|
||||||
slog.Debug("peer connection state changed",
|
slog.Debug("peer connection state changed", "peer", peer.Identifier, "connected", p.IsConnected)
|
||||||
"peer", peer.Identifier, "connected", p.IsConnected)
|
|
||||||
connectionStateChanged = true
|
connectionStateChanged = true
|
||||||
newPeerStatus = *p // store new status for event publishing
|
newPeerStatus = *p // store new status for event publishing
|
||||||
}
|
}
|
||||||
@@ -222,9 +203,6 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) {
|
|||||||
// Update prometheus metrics
|
// Update prometheus metrics
|
||||||
go c.updatePeerMetrics(ctx, *p)
|
go c.updatePeerMetrics(ctx, *p)
|
||||||
|
|
||||||
// Publish stats update event
|
|
||||||
c.bus.Publish(app.TopicPeerStatsUpdated, td)
|
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -985,26 +985,7 @@ func (m Manager) importPeer(ctx context.Context, in *domain.Interface, p *domain
|
|||||||
peer.InterfaceIdentifier = in.Identifier
|
peer.InterfaceIdentifier = in.Identifier
|
||||||
peer.EndpointPublicKey = domain.NewConfigOption(in.PublicKey, true)
|
peer.EndpointPublicKey = domain.NewConfigOption(in.PublicKey, true)
|
||||||
peer.AllowedIPsStr = domain.NewConfigOption(in.PeerDefAllowedIPsStr, true)
|
peer.AllowedIPsStr = domain.NewConfigOption(in.PeerDefAllowedIPsStr, true)
|
||||||
|
peer.Interface.Addresses = p.AllowedIPs // use allowed IP's as the peer IP's TODO: Should this also match server interface address' prefix length?
|
||||||
// split allowed IP's into interface addresses and extra allowed IP's
|
|
||||||
var interfaceAddresses []domain.Cidr
|
|
||||||
var extraAllowedIPs []domain.Cidr
|
|
||||||
for _, allowedIP := range p.AllowedIPs {
|
|
||||||
isHost := (allowedIP.IsV4() && allowedIP.NetLength == 32) || (!allowedIP.IsV4() && allowedIP.NetLength == 128)
|
|
||||||
isNetworkAddr := allowedIP.Addr == allowedIP.NetworkAddr().Addr
|
|
||||||
|
|
||||||
// Network addresses (e.g. 10.0.0.0/24) will always be extra allowed IP's.
|
|
||||||
// For IP addresses, such as 10.0.0.1/24, it is challenging to tell whether it is an interface address or
|
|
||||||
// an extra allowed IP, therefore we treat such addresses as interface addresses.
|
|
||||||
if !isHost && isNetworkAddr {
|
|
||||||
extraAllowedIPs = append(extraAllowedIPs, allowedIP)
|
|
||||||
} else {
|
|
||||||
interfaceAddresses = append(interfaceAddresses, allowedIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
peer.Interface.Addresses = interfaceAddresses
|
|
||||||
peer.ExtraAllowedIPsStr = domain.CidrsToString(extraAllowedIPs)
|
|
||||||
|
|
||||||
peer.Interface.DnsStr = domain.NewConfigOption(in.PeerDefDnsStr, true)
|
peer.Interface.DnsStr = domain.NewConfigOption(in.PeerDefDnsStr, true)
|
||||||
peer.Interface.DnsSearchStr = domain.NewConfigOption(in.PeerDefDnsSearchStr, true)
|
peer.Interface.DnsSearchStr = domain.NewConfigOption(in.PeerDefDnsSearchStr, true)
|
||||||
peer.Interface.Mtu = domain.NewConfigOption(in.PeerDefMtu, true)
|
peer.Interface.Mtu = domain.NewConfigOption(in.PeerDefMtu, true)
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
package wireguard
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/h44z/wg-portal/internal/domain"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestImportPeer_AddressMapping(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
allowedIPs []string
|
|
||||||
expectedInterface []string
|
|
||||||
expectedExtraAllowed string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "IPv4 host address",
|
|
||||||
allowedIPs: []string{"10.0.0.1/32"},
|
|
||||||
expectedInterface: []string{"10.0.0.1/32"},
|
|
||||||
expectedExtraAllowed: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv6 host address",
|
|
||||||
allowedIPs: []string{"fd00::1/128"},
|
|
||||||
expectedInterface: []string{"fd00::1/128"},
|
|
||||||
expectedExtraAllowed: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv4 network address",
|
|
||||||
allowedIPs: []string{"10.0.1.0/24"},
|
|
||||||
expectedInterface: []string{},
|
|
||||||
expectedExtraAllowed: "10.0.1.0/24",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "IPv4 normal address with mask",
|
|
||||||
allowedIPs: []string{"10.0.1.5/24"},
|
|
||||||
expectedInterface: []string{"10.0.1.5/24"},
|
|
||||||
expectedExtraAllowed: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Mixed addresses",
|
|
||||||
allowedIPs: []string{
|
|
||||||
"10.0.0.1/32", "192.168.1.0/24", "172.16.0.5/24", "fd00::1/128", "fd00:1::/64",
|
|
||||||
},
|
|
||||||
expectedInterface: []string{"10.0.0.1/32", "172.16.0.5/24", "fd00::1/128"},
|
|
||||||
expectedExtraAllowed: "192.168.1.0/24,fd00:1::/64",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
db := &mockDB{}
|
|
||||||
m := Manager{
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
|
|
||||||
iface := &domain.Interface{
|
|
||||||
Identifier: "wg0",
|
|
||||||
Type: domain.InterfaceTypeServer,
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedIPs := make([]domain.Cidr, len(tt.allowedIPs))
|
|
||||||
for i, s := range tt.allowedIPs {
|
|
||||||
cidr, _ := domain.CidrFromString(s)
|
|
||||||
allowedIPs[i] = cidr
|
|
||||||
}
|
|
||||||
|
|
||||||
p := &domain.PhysicalPeer{
|
|
||||||
Identifier: "peer1",
|
|
||||||
KeyPair: domain.KeyPair{PublicKey: "peer1-public-key-is-long-enough"},
|
|
||||||
AllowedIPs: allowedIPs,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := m.importPeer(context.Background(), iface, p)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
savedPeer := db.savedPeers["peer1"]
|
|
||||||
assert.NotNil(t, savedPeer)
|
|
||||||
|
|
||||||
// Check interface addresses
|
|
||||||
actualInterface := make([]string, len(savedPeer.Interface.Addresses))
|
|
||||||
for i, addr := range savedPeer.Interface.Addresses {
|
|
||||||
actualInterface[i] = addr.String()
|
|
||||||
}
|
|
||||||
assert.ElementsMatch(t, tt.expectedInterface, actualInterface)
|
|
||||||
|
|
||||||
// Check extra allowed IPs
|
|
||||||
assert.Equal(t, tt.expectedExtraAllowed, savedPeer.ExtraAllowedIPsStr)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -61,25 +61,3 @@ func (r PingerResult) AverageRtt() time.Duration {
|
|||||||
}
|
}
|
||||||
return total / time.Duration(len(r.Rtts))
|
return total / time.Duration(len(r.Rtts))
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrafficDelta struct {
|
|
||||||
EntityId string `json:"EntityId"` // Either peerId or interfaceId
|
|
||||||
BytesReceivedPerSecond uint64 `json:"BytesReceived"`
|
|
||||||
BytesTransmittedPerSecond uint64 `json:"BytesTransmitted"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func CalculateTrafficDelta(id string, oldTime, newTime time.Time, oldTx, newTx, oldRx, newRx uint64) TrafficDelta {
|
|
||||||
timeDiff := uint64(newTime.Sub(oldTime).Seconds())
|
|
||||||
if timeDiff == 0 {
|
|
||||||
return TrafficDelta{
|
|
||||||
EntityId: id,
|
|
||||||
BytesReceivedPerSecond: 0,
|
|
||||||
BytesTransmittedPerSecond: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return TrafficDelta{
|
|
||||||
EntityId: id,
|
|
||||||
BytesReceivedPerSecond: (newRx - oldRx) / timeDiff,
|
|
||||||
BytesTransmittedPerSecond: (newTx - oldTx) / timeDiff,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user