From 9e324639b5bebac9ad83e3a3420e469de85aa08b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 6 Oct 2025 12:03:31 -0400 Subject: [PATCH] Add network monitoring module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- go.mod | 2 +- internal/netmon/events.go | 87 +++++ internal/netmon/monitor.go | 65 ++++ internal/netmon/monitor_android.go | 332 +++++++++++++++++ internal/netmon/monitor_darwin.go | 370 +++++++++++++++++++ internal/netmon/monitor_linux.go | 547 +++++++++++++++++++++++++++++ internal/netmon/monitor_stub.go | 281 +++++++++++++++ internal/netmon/monitor_test.go | 43 +++ internal/netmon/monitor_windows.go | 337 ++++++++++++++++++ 9 files changed, 2063 insertions(+), 1 deletion(-) create mode 100644 internal/netmon/events.go create mode 100644 internal/netmon/monitor.go create mode 100644 internal/netmon/monitor_android.go create mode 100644 internal/netmon/monitor_darwin.go create mode 100644 internal/netmon/monitor_linux.go create mode 100644 internal/netmon/monitor_stub.go create mode 100644 internal/netmon/monitor_test.go create mode 100644 internal/netmon/monitor_windows.go diff --git a/go.mod b/go.mod index 8c115152..8af72d2a 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/pion/turn/v4 v4.1.1 github.com/stretchr/testify v1.11.1 golang.org/x/net v0.34.0 + golang.org/x/sys v0.30.0 ) require ( @@ -21,7 +22,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/wlynxg/anet v0.0.3 // indirect golang.org/x/crypto v0.32.0 // indirect - golang.org/x/sys v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/netmon/events.go b/internal/netmon/events.go new file mode 100644 index 00000000..9210a891 --- /dev/null +++ b/internal/netmon/events.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package netmon + +import ( + "fmt" + "net/netip" + "time" +) + +// EventType represents the type of network change event. +type EventType int + +const ( + // InterfaceAdded indicates a new network interface was added. + InterfaceAdded EventType = iota + // InterfaceRemoved indicates a network interface was removed. + InterfaceRemoved + // AddressAdded indicates an IP address was added to an interface. + AddressAdded + // AddressRemoved indicates an IP address was removed from an interface. + AddressRemoved + // StateChanged indicates the operational state of an interface changed. + StateChanged + // LinkChanged indicates link-level changes (e.g., cable plugged/unplugged). + LinkChanged +) + +// String returns a string representation of the event type. +func (e EventType) String() string { + switch e { + case InterfaceAdded: + return "InterfaceAdded" + case InterfaceRemoved: + return "InterfaceRemoved" + case AddressAdded: + return "AddressAdded" + case AddressRemoved: + return "AddressRemoved" + case StateChanged: + return "StateChanged" + case LinkChanged: + return "LinkChanged" + default: + return fmt.Sprintf("Unknown(%d)", e) + } +} + +// NetworkEvent represents a network change event. +type NetworkEvent struct { + Type EventType // Type of event + Interface NetworkInterface // Affected interface + Timestamp time.Time // When the event occurred + Details map[string]any // Platform-specific details + + // Additional fields for specific event types + OldState InterfaceState // Previous state (for StateChanged) + NewState InterfaceState // New state (for StateChanged) + Address netip.Addr // Affected address (for AddressAdded/Removed) +} + +// String returns a string representation of the network event. +func (e NetworkEvent) String() string { + switch e.Type { + case InterfaceAdded: + return fmt.Sprintf("[%s] Interface added: %s", e.Timestamp.Format("15:04:05"), e.Interface.Name) + case InterfaceRemoved: + return fmt.Sprintf("[%s] Interface removed: %s", e.Timestamp.Format("15:04:05"), e.Interface.Name) + case AddressAdded: + return fmt.Sprintf("[%s] Address added: %s on %s", e.Timestamp.Format("15:04:05"), e.Address, e.Interface.Name) + case AddressRemoved: + return fmt.Sprintf("[%s] Address removed: %s from %s", e.Timestamp.Format("15:04:05"), e.Address, e.Interface.Name) + case StateChanged: + return fmt.Sprintf( + "[%s] State changed: %s %s -> %s", + e.Timestamp.Format("15:04:05"), + e.Interface.Name, + e.OldState, + e.NewState, + ) + case LinkChanged: + return fmt.Sprintf("[%s] Link changed: %s", e.Timestamp.Format("15:04:05"), e.Interface.Name) + default: + return fmt.Sprintf("[%s] %s: %s", e.Timestamp.Format("15:04:05"), e.Type, e.Interface.Name) + } +} diff --git a/internal/netmon/monitor.go b/internal/netmon/monitor.go new file mode 100644 index 00000000..16b762ba --- /dev/null +++ b/internal/netmon/monitor.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +// Package netmon provides event-driven network interface monitoring +// across different operating systems. +package netmon + +import ( + "net/netip" +) + +// NetworkMonitor provides platform-specific network change monitoring. +type NetworkMonitor interface { + // Start begins monitoring network changes indefinitely until Close is called + Start() error + + // Events returns a channel for network change events + Events() <-chan NetworkEvent + + // GetInterfaces returns current network interfaces + GetInterfaces() ([]NetworkInterface, error) + + // Close stops monitoring and releases resources + Close() error +} + +// NetworkInterface represents a network interface with its properties. +type NetworkInterface struct { + Name string // Interface name (e.g., "eth0", "en0") + Index int // Interface index + Addresses []netip.Addr // IP addresses assigned to this interface + State InterfaceState // Current state of the interface + MTU int // Maximum transmission unit + Flags uint32 // Interface flags (platform-specific) + HWAddr []byte // Hardware address (MAC) +} + +// InterfaceState represents the operational state of a network interface. +type InterfaceState int + +const ( + // StateDown indicates the interface is administratively down. + StateDown InterfaceState = iota + // StateUp indicates the interface is up and operational. + StateUp + // StateUnknown indicates the state cannot be determined. + StateUnknown +) + +// String returns a string representation of the interface state. +func (s InterfaceState) String() string { + switch s { + case StateDown: + return "down" + case StateUp: + return "up" + default: + return "unknown" + } +} + +// New creates a new NetworkMonitor for the current platform. +func New() NetworkMonitor { + return newPlatformMonitor() +} diff --git a/internal/netmon/monitor_android.go b/internal/netmon/monitor_android.go new file mode 100644 index 00000000..b12ad842 --- /dev/null +++ b/internal/netmon/monitor_android.go @@ -0,0 +1,332 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build android +// +build android + +package netmon + +import ( + "context" + "net" + "net/netip" + "sync" + "time" + + "golang.org/x/sys/unix" +) + +// androidMonitor implements NetworkMonitor for Android +// Android inherits Linux netlink capabilities but may have restrictions +// This implementation falls back to polling with /proc/net parsing +type androidMonitor struct { + events chan NetworkEvent + interfaces map[int]*NetworkInterface + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + closeOnce sync.Once +} + +// newPlatformMonitor creates a new Android-specific network monitor +func newPlatformMonitor() NetworkMonitor { + // Try to use Linux netlink first + linuxMon := &linuxMonitor{ + events: make(chan NetworkEvent, 100), + interfaces: make(map[int]*NetworkInterface), + } + + // Test if netlink is available + sock, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW, unix.NETLINK_ROUTE) + if err == nil { + unix.Close(sock) + // Netlink is available, use Linux implementation + return linuxMon + } + + // Fall back to Android-specific implementation + return &androidMonitor{ + events: make(chan NetworkEvent, 100), + interfaces: make(map[int]*NetworkInterface), + } +} + +// Start begins monitoring network changes +func (m *androidMonitor) Start() error { + // Initialize current interfaces + if err := m.refreshInterfaces(); err != nil { + return err + } + + // Create internal context for lifecycle management + m.ctx, m.cancel = context.WithCancel(context.Background()) + + // Start monitoring goroutine + m.wg.Add(1) + go m.monitor() + + return nil +} + +// monitor runs the main monitoring loop with polling +func (m *androidMonitor) monitor() { + defer m.wg.Done() + + // Poll more frequently on Android as we don't have real-time events + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + m.checkInterfaceChanges() + } + } +} + +// checkInterfaceChanges compares current interfaces with cached ones +func (m *androidMonitor) checkInterfaceChanges() { + newInterfaces := make(map[int]*NetworkInterface) + + // Get current interfaces + ifaces, err := net.Interfaces() + if err != nil { + return + } + + for _, iface := range ifaces { + // Skip certain Android virtual interfaces + if shouldSkipAndroidInterface(iface.Name) { + continue + } + + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + newInterfaces[iface.Index] = netIface + } + + m.mu.Lock() + defer m.mu.Unlock() + + // Check for removed interfaces + for idx, oldIface := range m.interfaces { + if _, exists := newInterfaces[idx]; !exists { + event := NetworkEvent{ + Type: InterfaceRemoved, + Interface: *oldIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for new or changed interfaces + for idx, newIface := range newInterfaces { + oldIface, exists := m.interfaces[idx] + if !exists { + // New interface + event := NetworkEvent{ + Type: InterfaceAdded, + Interface: *newIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } else { + // Check for state changes + if oldIface.State != newIface.State { + event := NetworkEvent{ + Type: StateChanged, + Interface: *newIface, + OldState: oldIface.State, + NewState: newIface.State, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + + // Check for address changes + oldAddrs := make(map[netip.Addr]bool) + for _, addr := range oldIface.Addresses { + oldAddrs[addr] = true + } + + newAddrs := make(map[netip.Addr]bool) + for _, addr := range newIface.Addresses { + newAddrs[addr] = true + } + + // Check for removed addresses + for addr := range oldAddrs { + if !newAddrs[addr] { + event := NetworkEvent{ + Type: AddressRemoved, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for added addresses + for addr := range newAddrs { + if !oldAddrs[addr] { + event := NetworkEvent{ + Type: AddressAdded, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + } + } + + // Update interfaces map + m.interfaces = newInterfaces +} + +// Events returns the event channel +func (m *androidMonitor) Events() <-chan NetworkEvent { + return m.events +} + +// GetInterfaces returns current network interfaces +func (m *androidMonitor) GetInterfaces() ([]NetworkInterface, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + interfaces := make([]NetworkInterface, 0, len(m.interfaces)) + for _, iface := range m.interfaces { + interfaces = append(interfaces, *iface) + } + + return interfaces, nil +} + +// Close stops monitoring and releases resources +func (m *androidMonitor) Close() error { + m.closeOnce.Do(func() { + if m.cancel != nil { + m.cancel() + } + + m.wg.Wait() + close(m.events) + }) + + return nil +} + +// refreshInterfaces updates the current interface list +func (m *androidMonitor) refreshInterfaces() error { + ifaces, err := net.Interfaces() + if err != nil { + return err + } + + m.mu.Lock() + defer m.mu.Unlock() + + for _, iface := range ifaces { + // Skip certain Android virtual interfaces + if shouldSkipAndroidInterface(iface.Name) { + continue + } + + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + m.interfaces[iface.Index] = netIface + } + + return nil +} + +// shouldSkipAndroidInterface returns true if the interface should be ignored +func shouldSkipAndroidInterface(name string) bool { + // Skip common Android virtual interfaces that don't represent real network connectivity + skipPrefixes := []string{ + "dummy", // Dummy interfaces + "tunl", // Tunnel interfaces + "sit", // IPv6-in-IPv4 tunnel + "ip6tnl", // IPv6 tunnel + "ip6gre", // IPv6 GRE tunnel + "teql", // Traffic equalizer + "ip_vti", // Virtual tunnel interface + } + + for _, prefix := range skipPrefixes { + if len(name) >= len(prefix) && name[:len(prefix)] == prefix { + return true + } + } + + return false +} diff --git a/internal/netmon/monitor_darwin.go b/internal/netmon/monitor_darwin.go new file mode 100644 index 00000000..7ae50367 --- /dev/null +++ b/internal/netmon/monitor_darwin.go @@ -0,0 +1,370 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build darwin || ios +// +build darwin ios + +package netmon + +import ( + "context" + "net" + "net/netip" + "sync" + "syscall" + "time" + "unsafe" +) + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Network -framework Foundation +#import +#import + +typedef void* monitor_t; +typedef void* path_t; + +static monitor_t create_path_monitor() { + nw_path_monitor_t monitor = nw_path_monitor_create(); + return (void*)monitor; +} + +static void start_path_monitor(monitor_t mon, void* context) { + nw_path_monitor_t monitor = (nw_path_monitor_t)mon; + dispatch_queue_t queue = dispatch_queue_create("com.pion.netmon", DISPATCH_QUEUE_SERIAL); + + nw_path_monitor_set_queue(monitor, queue); + + nw_path_monitor_set_update_handler(monitor, ^(nw_path_t path) { + extern void pathUpdateCallback(void* context, void* path); + pathUpdateCallback(context, (void*)path); + }); + + nw_path_monitor_start(monitor); +} + +static void stop_path_monitor(monitor_t mon) { + nw_path_monitor_t monitor = (nw_path_monitor_t)mon; + nw_path_monitor_cancel(monitor); +} + +static int path_get_status(path_t p) { + nw_path_t path = (nw_path_t)p; + nw_path_status_t status = nw_path_get_status(path); + return (int)status; +} +*/ +import "C" + +// darwinMonitor implements NetworkMonitor using Network Framework +type darwinMonitor struct { + events chan NetworkEvent + interfaces map[int]*NetworkInterface + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + monitor C.monitor_t + closeOnce sync.Once +} + +// Path status constants from Network Framework +const ( + pathStatusInvalid = 0 + pathStatusSatisfied = 1 + pathStatusUnsatisfied = 2 + pathStatusSatisfiable = 3 +) + +var darwinMonitorInstance *darwinMonitor + +// newPlatformMonitor creates a new Darwin-specific network monitor +func newPlatformMonitor() NetworkMonitor { + return &darwinMonitor{ + events: make(chan NetworkEvent, 100), + interfaces: make(map[int]*NetworkInterface), + } +} + +// Start begins monitoring network changes using Network Framework +func (m *darwinMonitor) Start() error { + // Initialize current interfaces + if err := m.refreshInterfaces(); err != nil { + return err + } + + // Create internal context for lifecycle management + m.ctx, m.cancel = context.WithCancel(context.Background()) + + // Create path monitor + m.monitor = C.create_path_monitor() + + // Store instance for callback + darwinMonitorInstance = m + + // Start monitoring + C.start_path_monitor(m.monitor, unsafe.Pointer(m)) + + // Start polling goroutine as backup + m.wg.Add(1) + go m.pollInterfaces() + + return nil +} + +// pollInterfaces periodically checks for interface changes as a backup +func (m *darwinMonitor) pollInterfaces() { + defer m.wg.Done() + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + m.checkInterfaceChanges() + } + } +} + +// checkInterfaceChanges compares current interfaces with cached ones +func (m *darwinMonitor) checkInterfaceChanges() { + newInterfaces := make(map[int]*NetworkInterface) + + // Get current interfaces using syscalls + ifaces, err := net.Interfaces() + if err != nil { + return + } + + for _, iface := range ifaces { + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + newInterfaces[iface.Index] = netIface + } + + m.mu.Lock() + defer m.mu.Unlock() + + // Check for removed interfaces + for idx, oldIface := range m.interfaces { + if _, exists := newInterfaces[idx]; !exists { + event := NetworkEvent{ + Type: InterfaceRemoved, + Interface: *oldIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for new or changed interfaces + for idx, newIface := range newInterfaces { + oldIface, exists := m.interfaces[idx] + if !exists { + // New interface + event := NetworkEvent{ + Type: InterfaceAdded, + Interface: *newIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } else { + // Check for state changes + if oldIface.State != newIface.State { + event := NetworkEvent{ + Type: StateChanged, + Interface: *newIface, + OldState: oldIface.State, + NewState: newIface.State, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + + // Check for address changes + oldAddrs := make(map[netip.Addr]bool) + for _, addr := range oldIface.Addresses { + oldAddrs[addr] = true + } + + newAddrs := make(map[netip.Addr]bool) + for _, addr := range newIface.Addresses { + newAddrs[addr] = true + } + + // Check for removed addresses + for addr := range oldAddrs { + if !newAddrs[addr] { + event := NetworkEvent{ + Type: AddressRemoved, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for added addresses + for addr := range newAddrs { + if !oldAddrs[addr] { + event := NetworkEvent{ + Type: AddressAdded, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + } + } + + // Update interfaces map + m.interfaces = newInterfaces +} + +// Events returns the event channel +func (m *darwinMonitor) Events() <-chan NetworkEvent { + return m.events +} + +// GetInterfaces returns current network interfaces +func (m *darwinMonitor) GetInterfaces() ([]NetworkInterface, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + interfaces := make([]NetworkInterface, 0, len(m.interfaces)) + for _, iface := range m.interfaces { + interfaces = append(interfaces, *iface) + } + + return interfaces, nil +} + +// Close stops monitoring and releases resources +func (m *darwinMonitor) Close() error { + m.closeOnce.Do(func() { + if m.cancel != nil { + m.cancel() + } + + if m.monitor != nil { + C.stop_path_monitor(m.monitor) + } + + m.wg.Wait() + close(m.events) + }) + + return nil +} + +// refreshInterfaces updates the current interface list +func (m *darwinMonitor) refreshInterfaces() error { + ifaces, err := net.Interfaces() + if err != nil { + return err + } + + m.mu.Lock() + defer m.mu.Unlock() + + for _, iface := range ifaces { + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + m.interfaces[iface.Index] = netIface + } + + return nil +} + +// pathUpdateCallback is called from C when network path changes +// +//export pathUpdateCallback +func pathUpdateCallback(context unsafe.Pointer, path unsafe.Pointer) { + if darwinMonitorInstance != nil { + status := C.path_get_status(C.path_t(path)) + + // Trigger interface check when path changes + if status == pathStatusSatisfied || status == pathStatusUnsatisfied { + darwinMonitorInstance.checkInterfaceChanges() + } + } +} + +// Darwin-specific helper using route sockets as alternative +func listenRouteSocket() (int, error) { + fd, err := syscall.Socket(syscall.AF_ROUTE, syscall.SOCK_RAW, 0) + if err != nil { + return -1, err + } + return fd, nil +} diff --git a/internal/netmon/monitor_linux.go b/internal/netmon/monitor_linux.go new file mode 100644 index 00000000..c9a308c8 --- /dev/null +++ b/internal/netmon/monitor_linux.go @@ -0,0 +1,547 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build linux +// +build linux + +package netmon + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "net" + "net/netip" + "sync" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +// linuxMonitor implements NetworkMonitor using Linux netlink sockets. +type linuxMonitor struct { + sock int + events chan NetworkEvent + interfaces map[int]*NetworkInterface + mu sync.RWMutex + cancel context.CancelFunc + wg sync.WaitGroup + closeOnce sync.Once + done chan struct{} +} + +// newPlatformMonitor creates a new Linux-specific network monitor. +func newPlatformMonitor() NetworkMonitor { + return &linuxMonitor{ + events: make(chan NetworkEvent, 100), + interfaces: make(map[int]*NetworkInterface), + done: make(chan struct{}), + } +} + +// Start begins monitoring network changes using netlink. +func (m *linuxMonitor) Start() error { + // Create netlink socket. + sock, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW, unix.NETLINK_ROUTE) + if err != nil { + return fmt.Errorf("failed to create netlink socket: %w", err) + } + m.sock = sock + + // Bind to netlink groups for interface and address changes. + addr := &unix.SockaddrNetlink{ + Family: unix.AF_NETLINK, + Groups: unix.RTMGRP_LINK | unix.RTMGRP_IPV4_IFADDR | unix.RTMGRP_IPV6_IFADDR, + } + if err := unix.Bind(sock, addr); err != nil { + _ = unix.Close(sock) + + return fmt.Errorf("failed to bind netlink socket: %w", err) + } + + // Set socket to non-blocking mode. + if err := unix.SetNonblock(sock, true); err != nil { + _ = unix.Close(sock) + + return fmt.Errorf("failed to set non-blocking: %w", err) + } + + // Initialize current interfaces. + if err := m.refreshInterfaces(); err != nil { + _ = unix.Close(sock) + + return fmt.Errorf("failed to get initial interfaces: %w", err) + } + + // Create internal context for lifecycle management. + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + // Start monitoring goroutine. + m.wg.Add(1) + go m.monitor(ctx) + + return nil +} + +// monitor runs the main monitoring loop. +func (m *linuxMonitor) monitor(ctx context.Context) { + defer m.wg.Done() + + buf := make([]byte, 4096) + tv := unix.NsecToTimeval(int64(100 * time.Millisecond)) + + for { + select { + case <-ctx.Done(): + return + case <-m.done: + return + default: + } + + // Set receive timeout + _ = unix.SetsockoptTimeval(m.sock, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv) + + n, _, err := unix.Recvfrom(m.sock, buf, 0) + if err != nil { + m.handleRecvError(ctx, err) + + continue + } + + // Parse and process netlink messages + msgs := parseNetlinkMessages(buf[:n]) + for _, msg := range msgs { + m.processNetlinkMessage(msg) + } + } +} + +func (m *linuxMonitor) handleRecvError(ctx context.Context, err error) { + if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) { + return + } + // Check if context was canceled. + select { + case <-ctx.Done(): + case <-m.done: + default: + } +} + +// processNetlinkMessage handles individual netlink messages. +func (m *linuxMonitor) processNetlinkMessage(msg *netlinkMessage) { + switch msg.Header.Type { + case unix.RTM_NEWLINK: + m.handleLinkNew(msg) + case unix.RTM_DELLINK: + m.handleLinkDel(msg) + case unix.RTM_NEWADDR: + m.handleAddrNew(msg) + case unix.RTM_DELADDR: + m.handleAddrDel(msg) + } +} + +// handleLinkNew handles new interface events. +func (m *linuxMonitor) handleLinkNew(msg *netlinkMessage) { + // nolint:gosec // This is intentional netlink message parsing. + ifinfo := (*ifInfoMsg)(unsafe.Pointer(&msg.Data[0])) + attrs := parseLinkAttributes(msg.Data[unix.SizeofIfInfomsg:]) + + m.mu.Lock() + defer m.mu.Unlock() + + iface, exists := m.interfaces[int(ifinfo.Index)] + if !exists { + m.handleNewInterface(ifinfo, attrs) + + return + } + + // Check for state change. + m.handleStateChange(iface, ifinfo) +} + +func (m *linuxMonitor) handleNewInterface(ifinfo *ifInfoMsg, attrs map[uint16][]byte) { + iface := &NetworkInterface{ + Index: int(ifinfo.Index), + State: ifStateFromFlags(ifinfo.Flags), + Flags: ifinfo.Flags, + } + + if name, ok := attrs[unix.IFLA_IFNAME]; ok { + iface.Name = string(name[:len(name)-1]) // Remove null terminator + } + + if mtu, ok := attrs[unix.IFLA_MTU]; ok && len(mtu) >= 4 { + iface.MTU = int(binary.LittleEndian.Uint32(mtu)) + } + + if addr, ok := attrs[unix.IFLA_ADDRESS]; ok { + iface.HWAddr = addr + } + + m.interfaces[int(ifinfo.Index)] = iface + + event := NetworkEvent{ + Type: InterfaceAdded, + Interface: *iface, + Timestamp: time.Now(), + } + + select { + case m.events <- event: + default: + } +} + +func (m *linuxMonitor) handleStateChange(iface *NetworkInterface, ifinfo *ifInfoMsg) { + newState := ifStateFromFlags(ifinfo.Flags) + if iface.State == newState { + return + } + + oldState := iface.State + iface.State = newState + iface.Flags = ifinfo.Flags + + event := NetworkEvent{ + Type: StateChanged, + Interface: *iface, + OldState: oldState, + NewState: newState, + Timestamp: time.Now(), + } + + select { + case m.events <- event: + default: + } +} + +// handleLinkDel handles interface removal events. +func (m *linuxMonitor) handleLinkDel(msg *netlinkMessage) { + // nolint:gosec // This is intentional netlink message parsing. + ifinfo := (*ifInfoMsg)(unsafe.Pointer(&msg.Data[0])) + + m.mu.Lock() + defer m.mu.Unlock() + + if iface, exists := m.interfaces[int(ifinfo.Index)]; exists { + event := NetworkEvent{ + Type: InterfaceRemoved, + Interface: *iface, + Timestamp: time.Now(), + } + + delete(m.interfaces, int(ifinfo.Index)) + + select { + case m.events <- event: + default: + } + } +} + +// handleAddrNew handles new address events. +func (m *linuxMonitor) handleAddrNew(msg *netlinkMessage) { + // nolint:gosec // This is intentional netlink message parsing. + ifaddr := (*ifAddrMsg)(unsafe.Pointer(&msg.Data[0])) + attrs := parseAddrAttributes(msg.Data[unix.SizeofIfAddrmsg:]) + + m.mu.Lock() + defer m.mu.Unlock() + + iface, exists := m.interfaces[int(ifaddr.Index)] + if !exists { + return + } + + addr := m.extractAddress(attrs, ifaddr.Family) + if !addr.IsValid() { + return + } + + // Check if address already exists. + for _, existing := range iface.Addresses { + if existing == addr { + return + } + } + + iface.Addresses = append(iface.Addresses, addr) + + event := NetworkEvent{ + Type: AddressAdded, + Interface: *iface, + Address: addr, + Timestamp: time.Now(), + } + + select { + case m.events <- event: + default: + } +} + +func (m *linuxMonitor) extractAddress(attrs map[uint16][]byte, family uint8) netip.Addr { + addrData, ok := attrs[unix.IFA_LOCAL] + if !ok || addrData == nil { + addrData, ok = attrs[unix.IFA_ADDRESS] + if !ok { + return netip.Addr{} + } + } + + if family == unix.AF_INET && len(addrData) == 4 { + return netip.AddrFrom4([4]byte(addrData)) + } + + if family == unix.AF_INET6 && len(addrData) == 16 { + return netip.AddrFrom16([16]byte(addrData)) + } + + return netip.Addr{} +} + +// handleAddrDel handles address removal events. +func (m *linuxMonitor) handleAddrDel(msg *netlinkMessage) { + // nolint:gosec // This is intentional netlink message parsing. + ifaddr := (*ifAddrMsg)(unsafe.Pointer(&msg.Data[0])) + attrs := parseAddrAttributes(msg.Data[unix.SizeofIfAddrmsg:]) + + m.mu.Lock() + defer m.mu.Unlock() + + iface, exists := m.interfaces[int(ifaddr.Index)] + if !exists { + return + } + + addrData, ok := attrs[unix.IFA_ADDRESS] + if !ok { + return + } + + addr := m.parseAddrFromData(addrData, ifaddr.Family) + if !addr.IsValid() { + return + } + + // Remove address from interface. + newAddrs := make([]netip.Addr, 0, len(iface.Addresses)) + for _, existing := range iface.Addresses { + if existing != addr { + newAddrs = append(newAddrs, existing) + } + } + iface.Addresses = newAddrs + + event := NetworkEvent{ + Type: AddressRemoved, + Interface: *iface, + Address: addr, + Timestamp: time.Now(), + } + + select { + case m.events <- event: + default: + } +} + +func (m *linuxMonitor) parseAddrFromData(addrData []byte, family uint8) netip.Addr { + if family == unix.AF_INET && len(addrData) == 4 { + return netip.AddrFrom4([4]byte(addrData)) + } + + if family == unix.AF_INET6 && len(addrData) == 16 { + return netip.AddrFrom16([16]byte(addrData)) + } + + return netip.Addr{} +} + +// Events returns the event channel. +func (m *linuxMonitor) Events() <-chan NetworkEvent { + return m.events +} + +// GetInterfaces returns current network interfaces. +func (m *linuxMonitor) GetInterfaces() ([]NetworkInterface, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + interfaces := make([]NetworkInterface, 0, len(m.interfaces)) + for _, iface := range m.interfaces { + interfaces = append(interfaces, *iface) + } + + return interfaces, nil +} + +// Close stops monitoring and releases resources. +func (m *linuxMonitor) Close() error { + var closeErr error + + m.closeOnce.Do(func() { + close(m.done) + + if m.cancel != nil { + m.cancel() + } + + m.wg.Wait() + + if m.sock != 0 { + closeErr = unix.Close(m.sock) + m.sock = 0 + } + + close(m.events) + }) + + return closeErr +} + +// refreshInterfaces updates the current interface list. +func (m *linuxMonitor) refreshInterfaces() error { + ifaces, err := net.Interfaces() + if err != nil { + return err + } + + m.mu.Lock() + defer m.mu.Unlock() + + for _, iface := range ifaces { + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), // nolint:gosec // This is the standard conversion + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + m.interfaces[iface.Index] = netIface + } + + return nil +} + +// Helper structures and functions. + +// netlinkMessage represents a netlink message. +type netlinkMessage struct { + Header unix.NlMsghdr + Data []byte +} + +// ifInfoMsg represents interface information message. +type ifInfoMsg struct { + Family uint8 + _ uint8 + Type uint16 + Index int32 + Flags uint32 + Change uint32 +} + +// ifAddrMsg represents interface address message. +type ifAddrMsg struct { + Family uint8 + Prefixlen uint8 + Flags uint8 + Scope uint8 + Index uint32 +} + +// parseNetlinkMessages parses raw netlink data into messages. +func parseNetlinkMessages(data []byte) []*netlinkMessage { + var msgs []*netlinkMessage + + for len(data) >= unix.SizeofNlMsghdr { + // nolint:gosec // This is intentional netlink message parsing + header := (*unix.NlMsghdr)(unsafe.Pointer(&data[0])) + + if header.Len < unix.SizeofNlMsghdr || int(header.Len) > len(data) { + break + } + + msg := &netlinkMessage{ + Header: *header, + Data: data[unix.SizeofNlMsghdr:header.Len], + } + msgs = append(msgs, msg) + + // Align to 4-byte boundary + msgLen := int(header.Len) + alignedLen := (msgLen + unix.NLMSG_ALIGNTO - 1) & ^(unix.NLMSG_ALIGNTO - 1) + data = data[alignedLen:] + } + + return msgs +} + +// parseLinkAttributes parses link attributes from netlink message. +func parseLinkAttributes(data []byte) map[uint16][]byte { + attrs := make(map[uint16][]byte) + + for len(data) >= 4 { + // Read attribute header + attrLen := binary.LittleEndian.Uint16(data[0:2]) + attrType := binary.LittleEndian.Uint16(data[2:4]) + + if int(attrLen) < 4 || int(attrLen) > len(data) { + break + } + + // Extract attribute data + if attrLen > 4 { + attrs[attrType] = data[4:attrLen] + } + + // Align to 4-byte boundary + alignedLen := (int(attrLen) + 3) & ^3 + data = data[alignedLen:] + } + + return attrs +} + +// parseAddrAttributes parses address attributes from netlink message. +func parseAddrAttributes(data []byte) map[uint16][]byte { + return parseLinkAttributes(data) // Same format +} + +// ifStateFromFlags converts interface flags to state. +func ifStateFromFlags(flags uint32) InterfaceState { + if flags&syscall.IFF_UP != 0 { + return StateUp + } + + return StateDown +} diff --git a/internal/netmon/monitor_stub.go b/internal/netmon/monitor_stub.go new file mode 100644 index 00000000..65dcd79a --- /dev/null +++ b/internal/netmon/monitor_stub.go @@ -0,0 +1,281 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !linux && !windows && !darwin && !ios && !android +// +build !linux,!windows,!darwin,!ios,!android + +package netmon + +import ( + "context" + "net" + "net/netip" + "sync" + "time" +) + +// stubMonitor implements NetworkMonitor with polling for unsupported platforms +type stubMonitor struct { + events chan NetworkEvent + interfaces map[int]*NetworkInterface + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + closeOnce sync.Once +} + +// newPlatformMonitor creates a new stub network monitor for unsupported platforms +func newPlatformMonitor() NetworkMonitor { + return &stubMonitor{ + events: make(chan NetworkEvent, 100), + interfaces: make(map[int]*NetworkInterface), + } +} + +// Start begins monitoring network changes using polling +func (m *stubMonitor) Start() error { + // Initialize current interfaces + if err := m.refreshInterfaces(); err != nil { + return err + } + + // Create internal context for lifecycle management + m.ctx, m.cancel = context.WithCancel(context.Background()) + + // Start monitoring goroutine + m.wg.Add(1) + go m.monitor() + + return nil +} + +// monitor runs the main monitoring loop with polling +func (m *stubMonitor) monitor() { + defer m.wg.Done() + + // Poll every 2 seconds as a fallback + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + select { + case <-m.ctx.Done(): + return + case <-ticker.C: + m.checkInterfaceChanges() + } + } +} + +// checkInterfaceChanges compares current interfaces with cached ones +func (m *stubMonitor) checkInterfaceChanges() { + newInterfaces := make(map[int]*NetworkInterface) + + // Get current interfaces + ifaces, err := net.Interfaces() + if err != nil { + return + } + + for _, iface := range ifaces { + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + newInterfaces[iface.Index] = netIface + } + + m.mu.Lock() + defer m.mu.Unlock() + + // Check for removed interfaces + for idx, oldIface := range m.interfaces { + if _, exists := newInterfaces[idx]; !exists { + event := NetworkEvent{ + Type: InterfaceRemoved, + Interface: *oldIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for new or changed interfaces + for idx, newIface := range newInterfaces { + oldIface, exists := m.interfaces[idx] + if !exists { + // New interface + event := NetworkEvent{ + Type: InterfaceAdded, + Interface: *newIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } else { + // Check for state changes + if oldIface.State != newIface.State { + event := NetworkEvent{ + Type: StateChanged, + Interface: *newIface, + OldState: oldIface.State, + NewState: newIface.State, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + + // Check for address changes + oldAddrs := make(map[netip.Addr]bool) + for _, addr := range oldIface.Addresses { + oldAddrs[addr] = true + } + + newAddrs := make(map[netip.Addr]bool) + for _, addr := range newIface.Addresses { + newAddrs[addr] = true + } + + // Check for removed addresses + for addr := range oldAddrs { + if !newAddrs[addr] { + event := NetworkEvent{ + Type: AddressRemoved, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for added addresses + for addr := range newAddrs { + if !oldAddrs[addr] { + event := NetworkEvent{ + Type: AddressAdded, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + } + } + + // Update interfaces map + m.interfaces = newInterfaces +} + +// Events returns the event channel +func (m *stubMonitor) Events() <-chan NetworkEvent { + return m.events +} + +// GetInterfaces returns current network interfaces +func (m *stubMonitor) GetInterfaces() ([]NetworkInterface, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + interfaces := make([]NetworkInterface, 0, len(m.interfaces)) + for _, iface := range m.interfaces { + interfaces = append(interfaces, *iface) + } + + return interfaces, nil +} + +// Close stops monitoring and releases resources +func (m *stubMonitor) Close() error { + m.closeOnce.Do(func() { + if m.cancel != nil { + m.cancel() + } + + m.wg.Wait() + close(m.events) + }) + + return nil +} + +// refreshInterfaces updates the current interface list +func (m *stubMonitor) refreshInterfaces() error { + ifaces, err := net.Interfaces() + if err != nil { + return err + } + + m.mu.Lock() + defer m.mu.Unlock() + + for _, iface := range ifaces { + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + m.interfaces[iface.Index] = netIface + } + + return nil +} diff --git a/internal/netmon/monitor_test.go b/internal/netmon/monitor_test.go new file mode 100644 index 00000000..9e82d343 --- /dev/null +++ b/internal/netmon/monitor_test.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package netmon + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNetworkMonitor(t *testing.T) { + // Create a new monitor + monitor := New() + + // Start monitoring + err := monitor.Start() + assert.NoError(t, err, "Failed to start monitor") + + defer func() { + _ = monitor.Close() + }() + + // Get initial interfaces + interfaces, err := monitor.GetInterfaces() + assert.NoError(t, err, "Failed to get interfaces") + + if len(interfaces) == 0 { + t.Skip("No network interfaces found") + } + + // Verify we have at least one interface + t.Logf("Found %d network interfaces", len(interfaces)) + for _, iface := range interfaces { + t.Logf( + " Interface: %s (index=%d, state=%s, addresses=%d)", + iface.Name, + iface.Index, + iface.State, + len(iface.Addresses), + ) + } +} diff --git a/internal/netmon/monitor_windows.go b/internal/netmon/monitor_windows.go new file mode 100644 index 00000000..31ec0213 --- /dev/null +++ b/internal/netmon/monitor_windows.go @@ -0,0 +1,337 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +//go:build windows +// +build windows + +package netmon + +import ( + "context" + "net" + "net/netip" + "sync" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +// windowsMonitor implements NetworkMonitor using Windows APIs +type windowsMonitor struct { + events chan NetworkEvent + interfaces map[int]*NetworkInterface + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + overlapped *windows.Overlapped + changeHandle windows.Handle + closeOnce sync.Once +} + +// newPlatformMonitor creates a new Windows-specific network monitor +func newPlatformMonitor() NetworkMonitor { + return &windowsMonitor{ + events: make(chan NetworkEvent, 100), + interfaces: make(map[int]*NetworkInterface), + } +} + +// Start begins monitoring network changes using Windows APIs +func (m *windowsMonitor) Start() error { + // Initialize current interfaces + if err := m.refreshInterfaces(); err != nil { + return err + } + + // Create internal context for lifecycle management + m.ctx, m.cancel = context.WithCancel(context.Background()) + + // Create overlapped structure for async operations + m.overlapped = &windows.Overlapped{} + event, err := windows.CreateEvent(nil, 0, 0, nil) + if err != nil { + return err + } + m.overlapped.HEvent = event + + // Start monitoring goroutine + m.wg.Add(1) + go m.monitor() + + return nil +} + +// monitor runs the main monitoring loop +func (m *windowsMonitor) monitor() { + defer m.wg.Done() + defer windows.CloseHandle(m.overlapped.HEvent) + + for { + select { + case <-m.ctx.Done(): + return + default: + } + + // Register for address change notifications + var handle windows.Handle + err := notifyAddrChange(&handle, m.overlapped) + if err != nil && err != windows.ERROR_IO_PENDING { + time.Sleep(100 * time.Millisecond) + continue + } + + // Wait for notification or timeout + waitResult, err := windows.WaitForSingleObject(m.overlapped.HEvent, 100) + if err != nil { + continue + } + + if waitResult == windows.WAIT_OBJECT_0 { + // Network change detected + m.handleNetworkChange() + } + } +} + +// handleNetworkChange processes network change events +func (m *windowsMonitor) handleNetworkChange() { + newInterfaces := make(map[int]*NetworkInterface) + + // Get current interfaces + ifaces, err := net.Interfaces() + if err != nil { + return + } + + for _, iface := range ifaces { + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + newInterfaces[iface.Index] = netIface + } + + m.mu.Lock() + defer m.mu.Unlock() + + // Check for removed interfaces + for idx, oldIface := range m.interfaces { + if _, exists := newInterfaces[idx]; !exists { + event := NetworkEvent{ + Type: InterfaceRemoved, + Interface: *oldIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for new or changed interfaces + for idx, newIface := range newInterfaces { + oldIface, exists := m.interfaces[idx] + if !exists { + // New interface + event := NetworkEvent{ + Type: InterfaceAdded, + Interface: *newIface, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } else { + // Check for state changes + if oldIface.State != newIface.State { + event := NetworkEvent{ + Type: StateChanged, + Interface: *newIface, + OldState: oldIface.State, + NewState: newIface.State, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + + // Check for address changes + oldAddrs := make(map[netip.Addr]bool) + for _, addr := range oldIface.Addresses { + oldAddrs[addr] = true + } + + newAddrs := make(map[netip.Addr]bool) + for _, addr := range newIface.Addresses { + newAddrs[addr] = true + } + + // Check for removed addresses + for addr := range oldAddrs { + if !newAddrs[addr] { + event := NetworkEvent{ + Type: AddressRemoved, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + + // Check for added addresses + for addr := range newAddrs { + if !oldAddrs[addr] { + event := NetworkEvent{ + Type: AddressAdded, + Interface: *newIface, + Address: addr, + Timestamp: time.Now(), + } + select { + case m.events <- event: + default: + } + } + } + } + } + + // Update interfaces map + m.interfaces = newInterfaces +} + +// Events returns the event channel +func (m *windowsMonitor) Events() <-chan NetworkEvent { + return m.events +} + +// GetInterfaces returns current network interfaces +func (m *windowsMonitor) GetInterfaces() ([]NetworkInterface, error) { + m.mu.RLock() + defer m.mu.RUnlock() + + interfaces := make([]NetworkInterface, 0, len(m.interfaces)) + for _, iface := range m.interfaces { + interfaces = append(interfaces, *iface) + } + + return interfaces, nil +} + +// Close stops monitoring and releases resources +func (m *windowsMonitor) Close() error { + m.closeOnce.Do(func() { + if m.cancel != nil { + m.cancel() + } + + m.wg.Wait() + + if m.changeHandle != 0 { + windows.CloseHandle(m.changeHandle) + } + + close(m.events) + }) + + return nil +} + +// refreshInterfaces updates the current interface list +func (m *windowsMonitor) refreshInterfaces() error { + ifaces, err := net.Interfaces() + if err != nil { + return err + } + + m.mu.Lock() + defer m.mu.Unlock() + + for _, iface := range ifaces { + state := StateDown + if iface.Flags&net.FlagUp != 0 { + state = StateUp + } + + netIface := &NetworkInterface{ + Name: iface.Name, + Index: iface.Index, + State: state, + MTU: iface.MTU, + Flags: uint32(iface.Flags), + HWAddr: iface.HardwareAddr, + } + + // Get addresses + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + if ipnet, ok := addr.(*net.IPNet); ok { + if ip, ok := netip.AddrFromSlice(ipnet.IP); ok { + netIface.Addresses = append(netIface.Addresses, ip) + } + } + } + } + + m.interfaces[iface.Index] = netIface + } + + return nil +} + +// Windows-specific functions + +var ( + iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + procNotifyAddrChange = iphlpapi.NewProc("NotifyAddrChange") +) + +// notifyAddrChange registers for address change notifications +func notifyAddrChange(handle *windows.Handle, overlapped *windows.Overlapped) error { + r1, _, err := syscall.SyscallN( + procNotifyAddrChange.Addr(), + uintptr(unsafe.Pointer(handle)), + uintptr(unsafe.Pointer(overlapped)), + ) + if r1 != 0 { + if err != windows.ERROR_IO_PENDING { + return err + } + } + return nil +}