Files
wg-admin/internal/wireguard/status.go
Calmcacil 5d129562e2 fix: Properly parse compound handshake time and fix q key handling
- Fix parseHandshake to correctly parse number-unit pairs from '14 hours, 24 minutes, 40 seconds ago'
  Previous logic tried to parse unit words instead of finding associated numbers
  Now correctly accumulates all time units (hours, minutes, seconds)
- Fix q key handling to properly check screen type before quitting
  Only quit application when 'q' is pressed on list screen
  Other screens (detail, add, help) now handle 'q' to navigate back

Note: q key navigation may still need investigation for edge cases
2026-01-12 19:27:21 +01:00

220 lines
5.9 KiB
Go

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 5 minutes)
// This allows for ~12 missed keepalive intervals (at 25 seconds each)
if time.Since(peer.LatestHandshake) < 5*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()
var totalDuration time.Duration
parts := strings.Fields(handshake)
for i, part := range parts {
// Clean up commas
cleanPart := strings.TrimSuffix(part, ",")
// Check if this part is a time unit
if strings.HasSuffix(cleanPart, "second") || strings.HasSuffix(cleanPart, "seconds") {
// The number is the previous part
if i > 0 {
if val, err := strconv.Atoi(parts[i-1]); err == nil {
totalDuration += time.Duration(val) * time.Second
}
}
}
if strings.HasSuffix(cleanPart, "minute") || strings.HasSuffix(cleanPart, "minutes") {
if i > 0 {
if val, err := strconv.Atoi(parts[i-1]); err == nil {
totalDuration += time.Duration(val) * time.Minute
}
}
}
if strings.HasSuffix(cleanPart, "hour") || strings.HasSuffix(cleanPart, "hours") {
if i > 0 {
if val, err := strconv.Atoi(parts[i-1]); err == nil {
totalDuration += time.Duration(val) * time.Hour
}
}
}
if strings.HasSuffix(cleanPart, "day") || strings.HasSuffix(cleanPart, "days") {
if i > 0 {
if val, err := strconv.Atoi(parts[i-1]); err == nil {
totalDuration += time.Duration(val) * 24 * time.Hour
}
}
}
}
if totalDuration == 0 {
return now.Add(-time.Hour) // Default to 1 hour ago if parsing fails
}
return now.Add(-totalDuration)
}
// 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
}