Add WireGuard TUI implementation
- Add Go TUI with bubbletea for WireGuard management - Implement client CRUD operations with QR code generation - Add configuration and validation modules - Install/update scripts for client setup - Update Makefile to build binaries to bin/ directory - Add .gitignore for Go projects
This commit is contained in:
204
internal/wireguard/status.go
Normal file
204
internal/wireguard/status.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package wireguard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// StatusConnected indicates a peer has an active connection
|
||||
StatusConnected = "Connected"
|
||||
// StatusDisconnected indicates a peer is not connected
|
||||
StatusDisconnected = "Disconnected"
|
||||
)
|
||||
|
||||
// PeerStatus represents the status of a WireGuard peer
|
||||
type PeerStatus struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
AllowedIPs string `json:"allowed_ips"`
|
||||
LatestHandshake time.Time `json:"latest_handshake"`
|
||||
TransferRx string `json:"transfer_rx"`
|
||||
TransferTx string `json:"transfer_tx"`
|
||||
Status string `json:"status"` // "Connected" or "Disconnected"
|
||||
}
|
||||
|
||||
// GetClientStatus checks if a specific client is connected
|
||||
// Returns "Connected" if the peer appears in the active peers list, "Disconnected" otherwise
|
||||
func GetClientStatus(publicKey string) (string, error) {
|
||||
peers, err := GetAllPeers()
|
||||
if err != nil {
|
||||
return StatusDisconnected, fmt.Errorf("failed to get peer status: %w", err)
|
||||
}
|
||||
|
||||
for _, peer := range peers {
|
||||
if peer.PublicKey == publicKey {
|
||||
return peer.Status, nil
|
||||
}
|
||||
}
|
||||
|
||||
return StatusDisconnected, nil
|
||||
}
|
||||
|
||||
// GetAllPeers retrieves all peers with their current status from WireGuard
|
||||
func GetAllPeers() ([]PeerStatus, error) {
|
||||
output, err := exec.Command("wg", "show", "wg0").Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute wg show: %w", err)
|
||||
}
|
||||
|
||||
return parsePeersOutput(string(output)), nil
|
||||
}
|
||||
|
||||
// parsePeersOutput parses the output of 'wg show wg0' command
|
||||
func parsePeersOutput(output string) []PeerStatus {
|
||||
var peers []PeerStatus
|
||||
var currentPeer *PeerStatus
|
||||
var handshake string
|
||||
var transfer string
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
peerLineRegex := regexp.MustCompile(`^peer:\s*(.+)$`)
|
||||
handshakeRegex := regexp.MustCompile(`^latest handshake:\s*(.+)\s+ago$`)
|
||||
transferRegex := regexp.MustCompile(`^transfer:\s*(.+),\s+(.+)$`)
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Check for new peer
|
||||
if match := peerLineRegex.FindStringSubmatch(line); match != nil {
|
||||
// Save previous peer if exists
|
||||
if currentPeer != nil {
|
||||
peers = append(peers, finalizePeerStatus(currentPeer, handshake, transfer))
|
||||
}
|
||||
|
||||
// Start new peer
|
||||
currentPeer = &PeerStatus{
|
||||
PublicKey: match[1],
|
||||
}
|
||||
handshake = ""
|
||||
transfer = ""
|
||||
continue
|
||||
}
|
||||
|
||||
if currentPeer == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse endpoint
|
||||
if strings.HasPrefix(line, "endpoint:") {
|
||||
currentPeer.Endpoint = strings.TrimSpace(strings.TrimPrefix(line, "endpoint:"))
|
||||
}
|
||||
|
||||
// Parse allowed ips
|
||||
if strings.HasPrefix(line, "allowed ips:") {
|
||||
currentPeer.AllowedIPs = strings.TrimSpace(strings.TrimPrefix(line, "allowed ips:"))
|
||||
}
|
||||
|
||||
// Parse latest handshake
|
||||
if match := handshakeRegex.FindStringSubmatch(line); match != nil {
|
||||
handshake = match[1]
|
||||
}
|
||||
|
||||
// Parse transfer
|
||||
if match := transferRegex.FindStringSubmatch(line); match != nil {
|
||||
transfer = fmt.Sprintf("%s, %s", match[1], match[2])
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last peer
|
||||
if currentPeer != nil {
|
||||
peers = append(peers, finalizePeerStatus(currentPeer, handshake, transfer))
|
||||
}
|
||||
|
||||
return peers
|
||||
}
|
||||
|
||||
// finalizePeerStatus determines the peer's status based on handshake time
|
||||
func finalizePeerStatus(peer *PeerStatus, handshake string, transfer string) PeerStatus {
|
||||
peer.TransferRx = ""
|
||||
peer.TransferTx = ""
|
||||
|
||||
// Parse transfer
|
||||
if transfer != "" {
|
||||
parts := strings.Split(transfer, ", ")
|
||||
if len(parts) == 2 {
|
||||
// Extract received and sent values
|
||||
rxParts := strings.Fields(parts[0])
|
||||
if len(rxParts) >= 2 {
|
||||
peer.TransferRx = strings.Join(rxParts[:2], " ")
|
||||
}
|
||||
txParts := strings.Fields(parts[1])
|
||||
if len(txParts) >= 2 {
|
||||
peer.TransferTx = strings.Join(txParts[:2], " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine status based on handshake
|
||||
if handshake != "" {
|
||||
peer.LatestHandshake = parseHandshake(handshake)
|
||||
// Peer is considered connected if handshake is recent (within 3 minutes)
|
||||
if time.Since(peer.LatestHandshake) < 3*time.Minute {
|
||||
peer.Status = StatusConnected
|
||||
} else {
|
||||
peer.Status = StatusDisconnected
|
||||
}
|
||||
} else {
|
||||
peer.Status = StatusDisconnected
|
||||
}
|
||||
|
||||
return *peer
|
||||
}
|
||||
|
||||
// parseHandshake converts handshake string to time.Time
|
||||
func parseHandshake(handshake string) time.Time {
|
||||
now := time.Now()
|
||||
parts := strings.Fields(handshake)
|
||||
|
||||
for i, part := range parts {
|
||||
if strings.HasSuffix(part, "second") || strings.HasSuffix(part, "seconds") {
|
||||
if val, err := strconv.Atoi(strings.TrimSuffix(part, "s")); err == nil {
|
||||
return now.Add(-time.Duration(val) * time.Second)
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(part, "minute") || strings.HasSuffix(part, "minutes") {
|
||||
if val, err := strconv.Atoi(strings.TrimSuffix(part, "s")); err == nil {
|
||||
return now.Add(-time.Duration(val) * time.Minute)
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(part, "hour") || strings.HasSuffix(part, "hours") {
|
||||
if val, err := strconv.Atoi(strings.TrimSuffix(part, "s")); err == nil {
|
||||
return now.Add(-time.Duration(val) * time.Hour)
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(part, "day") || strings.HasSuffix(part, "days") {
|
||||
if val, err := strconv.Atoi(strings.TrimSuffix(part, "s")); err == nil {
|
||||
return now.Add(-time.Duration(val) * 24 * time.Hour)
|
||||
}
|
||||
}
|
||||
// Handle "ago" word
|
||||
if i > 0 && (part == "ago" || part == "ago,") {
|
||||
// Continue parsing time units
|
||||
}
|
||||
}
|
||||
|
||||
return now.Add(-time.Hour) // Default to 1 hour ago if parsing fails
|
||||
}
|
||||
|
||||
// CheckInterface verifies if the WireGuard interface exists and is accessible
|
||||
func CheckInterface(interfaceName string) error {
|
||||
cmd := exec.Command("wg", "show", interfaceName)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("wireguard interface '%s' not accessible: %w", interfaceName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user