From 0e61267ca05063d00c11b149f6e8e802daa0790d Mon Sep 17 00:00:00 2001 From: Edward Viaene Date: Fri, 23 Aug 2024 13:59:15 -0500 Subject: [PATCH] wireguard user stats --- pkg/configmanager/start_linux.go | 4 ++ pkg/configmanager/types.go | 4 +- pkg/storage/iface.go | 1 + pkg/storage/local/write.go | 13 ++++ pkg/testing/mocks/storage.go | 7 +++ pkg/wireguard/constants.go | 1 + pkg/wireguard/linux/constants.go | 6 ++ pkg/wireguard/linux/stats/types.go | 14 +++++ pkg/wireguard/linux/stats/usage.go | 38 +++++++++++ pkg/wireguard/stats_linux.go | 87 ++++++++++++++++++++++++++ pkg/wireguard/types.go | 11 ++++ pkg/wireguard/wireguardclientconfig.go | 24 ++++++- 12 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 pkg/wireguard/linux/constants.go create mode 100644 pkg/wireguard/linux/stats/types.go create mode 100644 pkg/wireguard/linux/stats/usage.go create mode 100644 pkg/wireguard/stats_linux.go diff --git a/pkg/configmanager/start_linux.go b/pkg/configmanager/start_linux.go index 5516755..d58a10a 100644 --- a/pkg/configmanager/start_linux.go +++ b/pkg/configmanager/start_linux.go @@ -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() } diff --git a/pkg/configmanager/types.go b/pkg/configmanager/types.go index f84fbf1..b7d8d54 100644 --- a/pkg/configmanager/types.go +++ b/pkg/configmanager/types.go @@ -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 diff --git a/pkg/storage/iface.go b/pkg/storage/iface.go index b8ae7bb..7cc8367 100644 --- a/pkg/storage/iface.go +++ b/pkg/storage/iface.go @@ -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 } diff --git a/pkg/storage/local/write.go b/pkg/storage/local/write.go index 1fabe26..1ca0508 100644 --- a/pkg/storage/local/write.go +++ b/pkg/storage/local/write.go @@ -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 +} diff --git a/pkg/testing/mocks/storage.go b/pkg/testing/mocks/storage.go index 31449b3..774823a 100644 --- a/pkg/testing/mocks/storage.go +++ b/pkg/testing/mocks/storage.go @@ -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() diff --git a/pkg/wireguard/constants.go b/pkg/wireguard/constants.go index 50a94ad..8b82074 100644 --- a/pkg/wireguard/constants.go +++ b/pkg/wireguard/constants.go @@ -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" diff --git a/pkg/wireguard/linux/constants.go b/pkg/wireguard/linux/constants.go new file mode 100644 index 0000000..3f66e71 --- /dev/null +++ b/pkg/wireguard/linux/constants.go @@ -0,0 +1,6 @@ +//go:build linux +// +build linux + +package wireguardlinux + +const VPN_INTERFACE_NAME = "vpn" diff --git a/pkg/wireguard/linux/stats/types.go b/pkg/wireguard/linux/stats/types.go new file mode 100644 index 0000000..8b27823 --- /dev/null +++ b/pkg/wireguard/linux/stats/types.go @@ -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"` +} diff --git a/pkg/wireguard/linux/stats/usage.go b/pkg/wireguard/linux/stats/usage.go new file mode 100644 index 0000000..e69ca10 --- /dev/null +++ b/pkg/wireguard/linux/stats/usage.go @@ -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 +} diff --git a/pkg/wireguard/stats_linux.go b/pkg/wireguard/stats_linux.go new file mode 100644 index 0000000..5d6abb8 --- /dev/null +++ b/pkg/wireguard/stats_linux.go @@ -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() +} diff --git a/pkg/wireguard/types.go b/pkg/wireguard/types.go index 41fbb48..dcd4cc0 100644 --- a/pkg/wireguard/types.go +++ b/pkg/wireguard/types.go @@ -2,6 +2,7 @@ package wireguard import ( "net/netip" + "time" ) type VPNClientData struct { @@ -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 +} diff --git a/pkg/wireguard/wireguardclientconfig.go b/pkg/wireguard/wireguardclientconfig.go index 9d03f0d..09a29f6 100644 --- a/pkg/wireguard/wireguardclientconfig.go +++ b/pkg/wireguard/wireguardclientconfig.go @@ -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) @@ -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"))