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 }