added Public IP monitoring and fixing issue where the app would stop recording session event until user logout.

This commit is contained in:
ghostersk
2025-05-28 06:16:50 +01:00
parent 795ab2839a
commit 52d20c8f0b
7 changed files with 288 additions and 48 deletions
+218 -21
View File
@@ -1,9 +1,13 @@
package session
import (
"context"
"fmt"
"io"
"net"
"net/http"
"os"
"strings"
"sync"
"syscall"
"time"
@@ -11,6 +15,7 @@ import (
"unsafe"
"monitoring-agent-win/api"
"monitoring-agent-win/config"
"monitoring-agent-win/logging"
"golang.org/x/sys/windows"
@@ -95,6 +100,10 @@ type Monitor struct {
mu sync.RWMutex
stopChan chan struct{}
hwnd HWND
// Add session notification health monitoring
lastNotificationTime time.Time
healthCheckTicker *time.Ticker
notificationHealthy bool
}
// NewMonitor creates a new session monitor
@@ -187,16 +196,23 @@ func (m *Monitor) Start() error {
}
m.hwnd = HWND(hwnd)
// Register for session notifications
if err := wtsRegisterSessionNotification(m.hwnd, NOTIFY_FOR_ALL_SESSIONS); err != nil {
destroyWindow(m.hwnd)
return fmt.Errorf("failed to register for session notifications: %v", err)
}
// Initialize health monitoring
m.lastNotificationTime = time.Now()
m.notificationHealthy = true
m.healthCheckTicker = time.NewTicker(5 * time.Minute) // Check every 5 minutes
// Start message loop in a goroutine
go m.messageLoop()
// Start health monitoring goroutine
go m.healthMonitor()
return nil
}
@@ -204,6 +220,11 @@ func (m *Monitor) Start() error {
func (m *Monitor) Stop() {
close(m.stopChan)
// Stop health monitoring
if m.healthCheckTicker != nil {
m.healthCheckTicker.Stop()
}
// Unregister session notifications and destroy window
if m.hwnd != 0 {
wtsUnRegisterSessionNotification(m.hwnd)
@@ -269,24 +290,29 @@ func (m *Monitor) messageLoop() {
func (m *Monitor) wndProc(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case WM_WTSSESSION_CHANGE:
// Capture timestamp immediately when event occurs for accuracy
eventTime := time.Now()
// Update last notification time for health monitoring
m.lastNotificationTime = eventTime
sessionID := uint32(lParam)
eventType := uint32(wParam)
// Get session info
username := m.getSessionUsername(sessionID)
computerName := getComputerName()
ipAddress := getDefaultIPv4()
localIP := getDefaultIPv4()
// Log the event
// Log the event with the captured timestamp
switch eventType {
case WTS_SESSION_LOGON:
m.logEvent(EventLogon, username, computerName, ipAddress)
m.logEventWithTime(EventLogon, username, computerName, localIP, eventTime)
case WTS_SESSION_LOGOFF:
m.logEvent(EventLogoff, username, computerName, ipAddress)
m.logEventWithTime(EventLogoff, username, computerName, localIP, eventTime)
case WTS_SESSION_LOCK:
m.logEvent(EventLock, username, computerName, ipAddress)
m.logEventWithTime(EventLock, username, computerName, localIP, eventTime)
case WTS_SESSION_UNLOCK:
m.logEvent(EventUnlock, username, computerName, ipAddress)
m.logEventWithTime(EventUnlock, username, computerName, localIP, eventTime)
}
}
@@ -332,31 +358,105 @@ func destroyWindow(hwnd HWND) {
proc.Call(uintptr(hwnd))
}
// logEvent logs an event to the CSV file and sends it to the API
func (m *Monitor) logEvent(eventType, username, computerName, ipAddress string) {
// Get current timestamp in the configured timezone
// getPublicIP retrieves public IP with HTTP first, then DNS fallback
func getPublicIP() (string, error) {
// Get config to check for custom HTTP URLs
cfg := config.GetConfig()
// Use custom URLs if configured, otherwise use defaults
urls := []string{
"https://ipv4.icanhazip.com",
"https://ifconfig.me/ip",
}
if len(cfg.PublicIPHTTPURLs) > 0 {
urls = cfg.PublicIPHTTPURLs
}
// Try HTTP services
client := &http.Client{Timeout: 2 * time.Second}
for _, url := range urls {
if resp, err := client.Get(url); err == nil && resp.StatusCode == 200 {
defer resp.Body.Close()
if body, err := io.ReadAll(resp.Body); err == nil {
if ip := strings.TrimSpace(string(body)); net.ParseIP(ip) != nil && net.ParseIP(ip).To4() != nil {
return ip, nil
}
}
}
}
// DNS fallback using Google's nameserver (equivalent to: dig TXT +short o-o.myaddr.l.google.com @ns1.google.com)
// First resolve ns1.google.com to get its IP dynamically
nsIPs, err := net.LookupHost("ns1.google.com")
if err != nil || len(nsIPs) == 0 {
return "", fmt.Errorf("failed to resolve ns1.google.com")
}
// Use the first IPv4 address from ns1.google.com
var nsIP string
for _, ip := range nsIPs {
if net.ParseIP(ip).To4() != nil {
nsIP = ip
break
}
}
if nsIP == "" {
return "", fmt.Errorf("no IPv4 address found for ns1.google.com")
}
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 2 * time.Second}
return d.DialContext(ctx, network, nsIP+":53")
},
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if txts, err := resolver.LookupTXT(ctx, "o-o.myaddr.l.google.com"); err == nil {
for _, txt := range txts {
if parsedIP := net.ParseIP(txt); parsedIP != nil && parsedIP.To4() != nil {
return txt, nil
}
}
}
return "", fmt.Errorf("failed to retrieve public IP")
}
// logEventWithTime logs an event with a pre-captured timestamp
func (m *Monitor) logEventWithTime(eventType, username, computerName, localIP string, eventTime time.Time) {
// Get public IP (this can be done in background as it's less time-sensitive)
publicIP, err := getPublicIP()
if err != nil {
m.logger.Warning("Failed to get public IP: %v - using 'Unknown'", err)
publicIP = "Unknown"
}
// Format the timestamp using the pre-captured event time
var timestamp string
if m.timezone != "" {
// Load the configured timezone
loc, err := time.LoadLocation(m.timezone)
// Try to load the configured timezone with Windows-specific handling
loc, err := loadTimezoneWithFallback(m.timezone)
if err != nil {
// If timezone loading fails, fall back to local time but log the error
m.logger.Warning("Failed to load timezone '%s': %v - using local time", m.timezone, err)
timestamp = time.Now().Format("2006-01-02T15:04:05-07:00")
timestamp = eventTime.Format("2006-01-02T15:04:05-07:00")
} else {
// Use the configured timezone
timestamp = time.Now().In(loc).Format("2006-01-02T15:04:05-07:00")
timestamp = eventTime.In(loc).Format("2006-01-02T15:04:05-07:00")
}
} else {
// If no timezone configured, use local time
timestamp = time.Now().Format("2006-01-02T15:04:05-07:00")
timestamp = eventTime.Format("2006-01-02T15:04:05-07:00")
}
// Log the event
m.logger.Info("User: %s - Event: %s - Computer: %s - IP: %s", username, eventType, computerName, ipAddress)
m.logger.Info("User: %s - Event: %s - Computer: %s - LocalIP: %s - PublicIP: %s", username, eventType, computerName, localIP, publicIP)
// Send to API
success, err := m.apiClient.SendEvent(eventType, username, computerName, ipAddress, timestamp, 0)
success, err := m.apiClient.SendEvent(eventType, username, computerName, localIP, publicIP, timestamp, 0)
if err != nil {
m.logger.Error("Failed to send event: %v", err)
}
@@ -367,7 +467,7 @@ func (m *Monitor) logEvent(eventType, username, computerName, ipAddress string)
sendStatus = logging.SendStatusFailed
}
if err := m.csvLogger.LogEvent(eventType, username, computerName, ipAddress, timestamp, sendStatus); err != nil {
if err := m.csvLogger.LogEvent(eventType, username, computerName, localIP, publicIP, timestamp, sendStatus); err != nil {
m.logger.Error("Failed to log event to CSV: %v", err)
}
@@ -399,7 +499,8 @@ func (m *Monitor) retryFailedEvents() {
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["localip"],
event["publicip"],
event["timestamp"],
1, // Retry flag
)
@@ -415,7 +516,8 @@ func (m *Monitor) retryFailedEvents() {
event["eventtype"],
event["username"],
event["hostname"],
event["ipaddress"],
event["localip"],
event["publicip"],
event["timestamp"],
logging.SendStatusRetrySuccess,
); err != nil {
@@ -617,3 +719,98 @@ func utf16PtrToString(p *uint16) string {
return string(utf16.Decode(s))
}
// loadTimezoneWithFallback loads timezone with Windows-specific handling
func loadTimezoneWithFallback(timezone string) (*time.Location, error) {
// First try the standard IANA timezone
loc, err := time.LoadLocation(timezone)
if err == nil {
return loc, nil
}
// Map common IANA timezones to Windows timezones
windowsTimezoneMap := map[string]string{
"Europe/London": "GMT Standard Time",
"Europe/Paris": "Romance Standard Time",
"Europe/Berlin": "W. Europe Standard Time",
"Europe/Madrid": "Romance Standard Time",
"Europe/Rome": "W. Europe Standard Time",
"Europe/Amsterdam": "W. Europe Standard Time",
"America/New_York": "Eastern Standard Time",
"America/Chicago": "Central Standard Time",
"America/Denver": "Mountain Standard Time",
"America/Los_Angeles": "Pacific Standard Time",
"Asia/Tokyo": "Tokyo Standard Time",
"Asia/Shanghai": "China Standard Time",
"Asia/Kolkata": "India Standard Time",
"Australia/Sydney": "AUS Eastern Standard Time",
"UTC": "UTC",
}
// Try Windows timezone name
if windowsName, exists := windowsTimezoneMap[timezone]; exists {
loc, err = time.LoadLocation(windowsName)
if err == nil {
return loc, nil
}
}
// If both fail, return the original error
return nil, fmt.Errorf("timezone '%s' not found (tried IANA and Windows formats)", timezone)
}
// reregisterSessionNotifications re-registers for session notifications
// This helps fix the issue where session notifications stop working
func (m *Monitor) reregisterSessionNotifications() error {
if m.hwnd == 0 {
return fmt.Errorf("no window handle available")
}
// Unregister first
wtsUnRegisterSessionNotification(m.hwnd)
// Small delay before re-registering
time.Sleep(100 * time.Millisecond)
// Re-register
err := wtsRegisterSessionNotification(m.hwnd, NOTIFY_FOR_ALL_SESSIONS)
if err != nil {
m.logger.Error("Failed to re-register session notifications: %v", err)
return err
}
m.logger.Info("Successfully re-registered session notifications")
return nil
}
// healthMonitor monitors session notification health and re-registers if needed
func (m *Monitor) healthMonitor() {
for {
select {
case <-m.stopChan:
if m.healthCheckTicker != nil {
m.healthCheckTicker.Stop()
}
return
case <-m.healthCheckTicker.C:
// Check if we've received any notifications in the last 10 minutes
// Note: This assumes some activity on the system, adjust threshold as needed
timeSinceLastNotification := time.Since(m.lastNotificationTime)
// If no notifications for more than 15 minutes and system isn't locked,
// consider re-registering (aggressive approach for testing)
if timeSinceLastNotification > 15*time.Minute {
m.logger.Warning("No session notifications received for %v, attempting re-registration", timeSinceLastNotification)
if err := m.reregisterSessionNotifications(); err != nil {
m.logger.Error("Failed to re-register session notifications: %v", err)
m.notificationHealthy = false
} else {
m.notificationHealthy = true
// Reset the timer to give the re-registration a chance
m.lastNotificationTime = time.Now()
}
}
}
}
}