- Fix 'q' key in detail/add/help screens to return to list instead of quitting - Only quit application with 'q' when on main list screen - Fix parseHandshake to accumulate all time units instead of returning early This resolves handshake timing discrepancy with wg show output - Increase connection status threshold from 3 to 5 minutes Allows ~12 missed keepalive intervals (25s each) Improves connectivity status accuracy for peers with marginal connections
211 lines
5.8 KiB
Go
211 lines
5.8 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 {
|
|
if strings.HasSuffix(part, "second") || strings.HasSuffix(part, "seconds") {
|
|
if val, err := strconv.Atoi(strings.TrimSuffix(part, "s")); err == nil {
|
|
totalDuration += 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 {
|
|
totalDuration += 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 {
|
|
totalDuration += 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 {
|
|
totalDuration += time.Duration(val) * 24 * time.Hour
|
|
}
|
|
}
|
|
// Handle "ago" word
|
|
if i > 0 && (part == "ago" || part == "ago,") {
|
|
// Continue parsing time units
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|