Skip to content

Commit

Permalink
wireguard user stats
Browse files Browse the repository at this point in the history
  • Loading branch information
wardviaene committed Aug 23, 2024
1 parent 108a0f7 commit 0e61267
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 2 deletions.
4 changes: 4 additions & 0 deletions pkg/configmanager/start_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ func startVPN(storage storage.Iface) error {
if err != nil {
log.Fatalf("WriteWireGuardServerConfig error: %s", err)
}

// run statistics go routine
go wireguard.RunStats(storage)

return wireguard.StartVPN()
}

Expand Down
4 changes: 3 additions & 1 deletion pkg/configmanager/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package configmanager

import "github.com/in4it/wireguard-server/pkg/storage"
import (
"github.com/in4it/wireguard-server/pkg/storage"
)

type ConfigManager struct {
PrivateKey string
Expand Down
1 change: 1 addition & 0 deletions pkg/storage/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Iface interface {
EnsureOwnership(filename, login string) error
ReadDir(name string) ([]string, error)
Remove(name string) error
AppendFile(name string, data []byte) error
ReadWriter
}

Expand Down
13 changes: 13 additions & 0 deletions pkg/storage/local/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,16 @@ import (
func (l *LocalStorage) WriteFile(name string, data []byte) error {
return os.WriteFile(path.Join(l.path, name), data, 0600)
}

func (l *LocalStorage) AppendFile(name string, data []byte) error {
f, err := os.OpenFile("text.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer f.Close()
if _, err := f.Write(data); err != nil {
return err
}

return nil
}
7 changes: 7 additions & 0 deletions pkg/testing/mocks/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ func (m *MockMemoryStorage) WriteFile(name string, data []byte) error {
m.Data[name] = data
return nil
}
func (m *MockMemoryStorage) AppendFile(name string, data []byte) error {
if m.Data == nil {
m.Data = make(map[string][]byte)
}
m.Data[name] = append(m.Data[name], data...)
return nil
}

func (m *MockMemoryStorage) GetPath() string {
pwd, _ := os.Executable()
Expand Down
1 change: 1 addition & 0 deletions pkg/wireguard/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const DEFAULT_VPN_PREFIX = "10.189.184.1/21"
const VPN_CONFIG_NAME = "vpn-config.json"
const IP_LIST_PATH = "config/iplist.json"
const VPN_CLIENTS_DIR = "clients"
const VPN_STATS_DIR = "stats"
const VPN_SERVER_SECRETS_PATH = "secrets"
const VPN_PRIVATE_KEY_FILENAME = "priv.key"
const PRESHARED_KEY_FILENAME = "preshared.key"
Expand Down
6 changes: 6 additions & 0 deletions pkg/wireguard/linux/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//go:build linux
// +build linux

package wireguardlinux

const VPN_INTERFACE_NAME = "vpn"
14 changes: 14 additions & 0 deletions pkg/wireguard/linux/stats/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build linux
// +build linux

package stats

import "time"

type PeerStat struct {
Timestamp time.Time `json:"timestamp"`
PublicKey string `json:"publicKey"`
LastHandshakeTime time.Time `json:"lastHandshakeTime"`
ReceiveBytes int64 `json:"receiveBytes"`
TransmitBytes int64 `json:"transmitBytes"`
}
38 changes: 38 additions & 0 deletions pkg/wireguard/linux/stats/usage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//go:build linux
// +build linux

package stats

import (
"fmt"
"time"

wireguardlinux "github.com/in4it/wireguard-server/pkg/wireguard/linux"
)

func GetStats() ([]PeerStat, error) {
c, available, err := wireguardlinux.New()
if err != nil {
return []PeerStat{}, fmt.Errorf("cannot start wireguardlinux client: %s", err)
}
if !available {
return []PeerStat{}, fmt.Errorf("wireguard linux client not available")
}
device, err := c.Device(wireguardlinux.VPN_INTERFACE_NAME)
if err != nil {
return []PeerStat{}, fmt.Errorf("wireguard linux device 'vpn' not found: %s", err)
}

peerStats := make([]PeerStat, len(device.Peers))

for k, peer := range device.Peers {
peerStats[k] = PeerStat{
Timestamp: time.Now(),
PublicKey: peer.PublicKey.String(),
LastHandshakeTime: peer.LastHandshakeTime,
ReceiveBytes: peer.ReceiveBytes,
TransmitBytes: peer.TransmitBytes,
}
}
return peerStats, nil
}
87 changes: 87 additions & 0 deletions pkg/wireguard/stats_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//go:build linux
// +build linux

package wireguard

import (
"bytes"
"fmt"
"path"
"strconv"
"strings"
"time"

"github.com/in4it/wireguard-server/pkg/logging"
"github.com/in4it/wireguard-server/pkg/storage"
"github.com/in4it/wireguard-server/pkg/wireguard/linux/stats"
)

const RUN_STATS_INTERVAL = 5
const TIMESTAMP_FORMAT = "2006-01-02T15:04:05"

func RunStats(storage storage.Iface) {
err := storage.EnsurePath(storage.ConfigPath(VPN_STATS_DIR))
if err != nil {
logging.ErrorLog(fmt.Errorf("could not create stats path: %s. Stats disabled", err))
return
}
for {
err := runStats(storage)
if err != nil {
logging.ErrorLog(fmt.Errorf("run stats error: %s", err))
}
time.Sleep(RUN_STATS_INTERVAL * time.Minute)
}
}

func runStats(storage storage.Iface) error {
peerStats, err := stats.GetStats()
if err != nil {
return fmt.Errorf("Could not get WireGuard stats: %s", err)
}

peerConfigs, err := GetAllPeerConfigs(storage)

statsEntries := []StatsEntry{}

for _, stat := range peerStats {
for _, peerConfig := range peerConfigs {
if stat.PublicKey == peerConfig.PublicKey {
user, connectionID := splitUserAndConnectionID(peerConfig.ID)
statsEntries = append(statsEntries, StatsEntry{
User: user,
ConnectionID: connectionID,
TransmitBytes: stat.TransmitBytes,
ReceiveBytes: stat.ReceiveBytes,
LastHandshakeTime: stat.LastHandshakeTime,
})
}
}
}

statsCsv := statsToCsv(statsEntries)

peerConfigPath := storage.ConfigPath(path.Join(VPN_STATS_DIR, "user-"+time.Now().Format("2006-01-02")))
err = storage.AppendFile(peerConfigPath, statsCsv)
if err != nil {
return fmt.Errorf("could not append stats to file (%s): %s", peerConfigPath, err)
}
return nil
}

func splitUserAndConnectionID(id string) (string, string) {
split := strings.Split(id, "-")
if len(split) == 1 {
return id, ""
}
return strings.Join(split[:len(split)-1], "-"), split[len(split)-1]
}

func statsToCsv(statsEntries []StatsEntry) []byte {
var res bytes.Buffer

for _, statsEntry := range statsEntries {
res.WriteString(strings.Join([]string{statsEntry.Timestamp.Format(TIMESTAMP_FORMAT), statsEntry.User, statsEntry.ConnectionID, strconv.FormatInt(statsEntry.ReceiveBytes, 10), strconv.FormatInt(statsEntry.TransmitBytes, 10), statsEntry.LastHandshakeTime.Format(TIMESTAMP_FORMAT)}, ",") + "\n")
}
return res.Bytes()
}
11 changes: 11 additions & 0 deletions pkg/wireguard/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package wireguard

import (
"net/netip"
"time"
)

type VPNClientData struct {
Expand Down Expand Up @@ -60,3 +61,13 @@ type RefreshClientRequest struct {
Action string
Filenames []string `json:"filenames"`
}

// stats
type StatsEntry struct {
Timestamp time.Time
User string
ConnectionID string
LastHandshakeTime time.Time
ReceiveBytes int64
TransmitBytes int64
}
24 changes: 23 additions & 1 deletion pkg/wireguard/wireguardclientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,12 @@ func UpdateClientsConfig(storage storage.Iface) error {
}

func getPeerConfig(storage storage.Iface, connectionID string) (PeerConfig, error) {
return getPeerConfigByFilename(storage, fmt.Sprintf("%s.json", connectionID))
}

func getPeerConfigByFilename(storage storage.Iface, filename string) (PeerConfig, error) {
var peerConfig PeerConfig
peerConfigFilename := storage.ConfigPath(path.Join(VPN_CLIENTS_DIR, fmt.Sprintf("%s.json", connectionID)))
peerConfigFilename := storage.ConfigPath(path.Join(VPN_CLIENTS_DIR, filename))
peerConfigBytes, err := storage.ReadFile(peerConfigFilename)
if err != nil {
return peerConfig, fmt.Errorf("cannot read connection config: %s", err)
Expand All @@ -174,6 +178,24 @@ func getPeerConfig(storage storage.Iface, connectionID string) (PeerConfig, erro
return peerConfig, nil
}

func GetAllPeerConfigs(storage storage.Iface) ([]PeerConfig, error) {
peerConfigPath := storage.ConfigPath(VPN_CLIENTS_DIR)

entries, err := storage.ReadDir(peerConfigPath)
if err != nil {
return []PeerConfig{}, fmt.Errorf("can not list clients from dir %s: %s", peerConfigPath, err)
}
peerConfigs := make([]PeerConfig, len(entries))
for k, entry := range entries {
peerConfig, err := getPeerConfigByFilename(storage, entry)
if err != nil {
return peerConfigs, fmt.Errorf("cnanot get peer config (%s): %s", entry, err)
}
peerConfigs[k] = peerConfig
}
return peerConfigs, nil
}

func GetClientTemplate(storage storage.Iface) ([]byte, error) {
filename := storage.ConfigPath("templates/client.tmpl")
err := storage.EnsurePath(storage.ConfigPath("templates"))
Expand Down

0 comments on commit 0e61267

Please sign in to comment.