mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
fix: count private network interfaces
This commit is contained in:
@@ -0,0 +1,363 @@
|
|||||||
|
# Windows Network Traffic Stats Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Fix Windows network traffic statistics showing zero when the active adapter is named like `Ethernet0` and uses a private IPv4 address.
|
||||||
|
|
||||||
|
**Architecture:** Keep the fix inside `internal/analytic/network.go`. Extract the interface selection rule into a small testable helper, then let `GetNetworkStat()` aggregate any up, non-loopback, non-virtual interface with a hardware address and at least one usable unicast IP address, including private IPv4 ranges. Continue excluding disconnected, loopback, link-local-only, TAP, tunnel, VPN, Docker, and similar virtual interfaces.
|
||||||
|
|
||||||
|
**Tech Stack:** Go, `net`, `github.com/shirou/gopsutil/v4/net`, existing `go test` workflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Modify: `internal/analytic/network.go`
|
||||||
|
- Keep `GetNetworkStat()` as the public entry point.
|
||||||
|
- Add a lightweight `networkInterfaceInfo` struct so selection logic can be unit-tested without depending on host NICs.
|
||||||
|
- Replace the current public-IP-only gate with `hasUsableUnicastIP()`.
|
||||||
|
- Create: `internal/analytic/network_test.go`
|
||||||
|
- Unit tests for Windows-style adapter names and private IPs.
|
||||||
|
- Regression tests for virtual, loopback, down, and link-local-only interfaces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Failing Network Interface Selection Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `internal/analytic/network_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create tests for the Windows regression and exclusions**
|
||||||
|
|
||||||
|
Create `internal/analytic/network_test.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdnet "net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustCIDR(t *testing.T, value string) stdnet.Addr {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ip, ipNet, err := stdnet.ParseCIDR(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse CIDR %q: %v", value, err)
|
||||||
|
}
|
||||||
|
ipNet.IP = ip
|
||||||
|
return ipNet
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceAcceptsWindowsEthernetPrivateIPv4(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "Ethernet0",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0x50, 0x56, 0xba, 0x25, 0x01},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.100.1.10/28")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected Windows Ethernet interface with private IPv4 to be counted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceRejectsTapAdapter(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "TAP-Windows Adapter V9",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0xff, 0x55, 0x61, 0x3a, 0xd2},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.8.0.2/24")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected TAP adapter to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceRejectsLinkLocalOnly(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "Ethernet0",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0x50, 0x56, 0xba, 0x25, 0x01},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "fe80::c562:f8dc:9cd4:18eb/64")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected link-local-only interface to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceRejectsLoopback(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "Loopback Pseudo-Interface 1",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagLoopback,
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "127.0.0.1/8")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected loopback interface to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests and verify they fail because the helper does not exist**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/analytic
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL with compile errors containing `undefined: networkInterfaceInfo` and `undefined: shouldCountNetworkInterface`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Implement Testable Interface Classification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `internal/analytic/network.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add testable interface metadata and selection helpers**
|
||||||
|
|
||||||
|
Add this near the top of `internal/analytic/network.go`, after imports and before `GetNetworkStat()`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type networkInterfaceInfo struct {
|
||||||
|
Name string
|
||||||
|
Flags stdnet.Flags
|
||||||
|
HardwareAddr stdnet.HardwareAddr
|
||||||
|
Addrs []stdnet.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldCountNetworkInterface(iface networkInterfaceInfo) bool {
|
||||||
|
if iface.Flags&stdnet.FlagUp == 0 || iface.Flags&stdnet.FlagLoopback != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isVirtualInterface(iface.Name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(iface.HardwareAddr) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUsableUnicastIP(iface.Addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCountedInterfaceSet(interfaces []networkInterfaceInfo) map[string]bool {
|
||||||
|
countedInterfaces := make(map[string]bool)
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
countedInterfaces[iface.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return countedInterfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasUsableUnicastIP(addrs []stdnet.Addr) bool {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ip, _, err := stdnet.ParseCIDR(addr.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ip.IsGlobalUnicast() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.IsLinkLocalUnicast() || ip.IsLoopback() || ip.IsMulticast() || ip.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isReservedIP(ip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests and verify helper behavior passes but `GetNetworkStat()` still uses old selection**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/analytic
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS for the new helper tests if they are the only changed tests; the runtime path has not been updated yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Wire `GetNetworkStat()` to the New Selection Rule
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `internal/analytic/network.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the existing external-interface discovery loop**
|
||||||
|
|
||||||
|
In `GetNetworkStat()`, replace the current logic that checks `isPhysicalInterface()` and `isRealExternalIP()` with this loop:
|
||||||
|
|
||||||
|
```go
|
||||||
|
interfaceInfos := make([]networkInterfaceInfo, 0, len(interfaces))
|
||||||
|
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceInfos = append(interfaceInfos, networkInterfaceInfo{
|
||||||
|
Name: iface.Name,
|
||||||
|
Flags: iface.Flags,
|
||||||
|
HardwareAddr: iface.HardwareAddr,
|
||||||
|
Addrs: addrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
countedInterfaces := buildCountedInterfaceSet(interfaceInfos)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update the aggregation condition from:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if externalInterfaces[stat.Name] {
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if countedInterfaces[stat.Name] {
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove now-unused physical/public-IP selection helpers**
|
||||||
|
|
||||||
|
Remove these functions if they are no longer referenced:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func isPhysicalInterface(name string) bool
|
||||||
|
func isNumericSuffix(s string) bool
|
||||||
|
func isRealExternalIP(ip stdnet.IP, ipNet *stdnet.IPNet) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep `isReservedIP()` because `hasUsableUnicastIP()` still uses it.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run gofmt**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofmt -w internal/analytic/network.go internal/analytic/network_test.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no output.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run analytic package tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/analytic
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Add Regression Coverage for Aggregation Name Matching
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `internal/analytic/network_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add a test for building the counted interface map**
|
||||||
|
|
||||||
|
Add this test:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestBuildCountedInterfaceSetIncludesOnlyEligibleNames(t *testing.T) {
|
||||||
|
interfaces := []networkInterfaceInfo{
|
||||||
|
{
|
||||||
|
Name: "Ethernet0",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0x50, 0x56, 0xba, 0x25, 0x01},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.100.1.10/28")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TAP-Windows Adapter V9",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0xff, 0x55, 0x61, 0x3a, 0xd2},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.8.0.2/24")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
counted := buildCountedInterfaceSet(interfaces)
|
||||||
|
if !counted["Ethernet0"] {
|
||||||
|
t.Fatalf("expected Ethernet0 to be counted")
|
||||||
|
}
|
||||||
|
if counted["TAP-Windows Adapter V9"] {
|
||||||
|
t.Fatalf("expected TAP adapter to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/analytic
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Final Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Verify only; no edits.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Check the changed file list**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --stat -- internal/analytic/network.go internal/analytic/network_test.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: only the network stat implementation and its tests are listed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run package tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/analytic
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Optional broader backend smoke test**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./api/analytic ./internal/analytic
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- Spec coverage: The plan addresses the observed Windows Server 2012 R2 output where `Ethernet0` has a private `10.100.x.x` address and traffic is currently filtered to zero.
|
||||||
|
- Risk control: The plan keeps virtual/tunnel exclusions and avoids switching blindly to `net.IOCounters(false)`, which could count loopback and virtual adapters.
|
||||||
|
- Test coverage: The plan adds regression tests for the Windows physical adapter case and for the exclusion cases that the old filter tried to protect.
|
||||||
+69
-131
@@ -8,6 +8,64 @@ import (
|
|||||||
"github.com/uozi-tech/cosy/logger"
|
"github.com/uozi-tech/cosy/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type networkInterfaceInfo struct {
|
||||||
|
Name string
|
||||||
|
Flags stdnet.Flags
|
||||||
|
HardwareAddr stdnet.HardwareAddr
|
||||||
|
Addrs []stdnet.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldCountNetworkInterface(iface networkInterfaceInfo) bool {
|
||||||
|
if iface.Flags&stdnet.FlagUp == 0 || iface.Flags&stdnet.FlagLoopback != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isVirtualInterface(iface.Name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(iface.HardwareAddr) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUsableUnicastIP(iface.Addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildCountedInterfaceSet(interfaces []networkInterfaceInfo) map[string]bool {
|
||||||
|
countedInterfaces := make(map[string]bool)
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
countedInterfaces[iface.Name] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return countedInterfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasUsableUnicastIP(addrs []stdnet.Addr) bool {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ip, _, err := stdnet.ParseCIDR(addr.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ip.IsGlobalUnicast() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.IsLinkLocalUnicast() || ip.IsLoopback() || ip.IsMulticast() || ip.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isReservedIP(ip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func GetNetworkStat() (data *net.IOCountersStat, err error) {
|
func GetNetworkStat() (data *net.IOCountersStat, err error) {
|
||||||
networkStats, err := net.IOCounters(true)
|
networkStats, err := net.IOCounters(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,60 +94,27 @@ func GetNetworkStat() (data *net.IOCountersStat, err error) {
|
|||||||
totalFifoOut uint64
|
totalFifoOut uint64
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a map of external interface names
|
interfaceInfos := make([]networkInterfaceInfo, 0, len(interfaces))
|
||||||
externalInterfaces := make(map[string]bool)
|
|
||||||
|
|
||||||
// Identify external interfaces
|
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
// Skip down or loopback interfaces
|
|
||||||
if iface.Flags&stdnet.FlagUp == 0 ||
|
|
||||||
iface.Flags&stdnet.FlagLoopback != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip common virtual interfaces by name pattern
|
|
||||||
if isVirtualInterface(iface.Name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a physical network interface
|
|
||||||
if isPhysicalInterface(iface.Name) && len(iface.HardwareAddr) > 0 {
|
|
||||||
externalInterfaces[iface.Name] = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get addresses for this interface
|
|
||||||
addrs, err := iface.Addrs()
|
addrs, err := iface.Addrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip interfaces without addresses
|
interfaceInfos = append(interfaceInfos, networkInterfaceInfo{
|
||||||
if len(addrs) == 0 {
|
Name: iface.Name,
|
||||||
continue
|
Flags: iface.Flags,
|
||||||
}
|
HardwareAddr: iface.HardwareAddr,
|
||||||
|
Addrs: addrs,
|
||||||
// Check for non-private IP addresses
|
})
|
||||||
for _, addr := range addrs {
|
|
||||||
ip, ipNet, err := stdnet.ParseCIDR(addr.String())
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip virtual, local, multicast, and special purpose IPs
|
|
||||||
if !isRealExternalIP(ip, ipNet) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
externalInterfaces[iface.Name] = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate stats only from external interfaces
|
countedInterfaces := buildCountedInterfaceSet(interfaceInfos)
|
||||||
|
|
||||||
|
// Accumulate stats only from counted interfaces
|
||||||
for _, stat := range networkStats {
|
for _, stat := range networkStats {
|
||||||
if externalInterfaces[stat.Name] {
|
if countedInterfaces[stat.Name] {
|
||||||
totalBytesRecv += stat.BytesRecv
|
totalBytesRecv += stat.BytesRecv
|
||||||
totalBytesSent += stat.BytesSent
|
totalBytesSent += stat.BytesSent
|
||||||
totalPacketsRecv += stat.PacketsRecv
|
totalPacketsRecv += stat.PacketsRecv
|
||||||
@@ -139,93 +164,6 @@ func isVirtualInterface(name string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPhysicalInterface checks if the interface is a physical network interface
|
|
||||||
// including server, cloud VM, and container physical interfaces
|
|
||||||
func isPhysicalInterface(name string) bool {
|
|
||||||
// Common prefixes for physical network interfaces across different platforms
|
|
||||||
physicalPrefixes := []string{
|
|
||||||
"eth", // Common Linux Ethernet interface
|
|
||||||
"en", // macOS and some Linux
|
|
||||||
"ens", // Predictable network interface names in systemd
|
|
||||||
"enp", // Predictable network interface names in systemd (PCI)
|
|
||||||
"eno", // Predictable network interface names in systemd (on-board)
|
|
||||||
"wlan", // Wireless interfaces
|
|
||||||
"wifi", // Some wireless interfaces
|
|
||||||
"wl", // Shortened wireless interfaces
|
|
||||||
"bond", // Bonded interfaces
|
|
||||||
"em", // Some server network interfaces
|
|
||||||
"p", // Some specialized network cards
|
|
||||||
"lan", // Some network interfaces
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for exact matches for common primary interfaces
|
|
||||||
if name == "eth0" || name == "en0" || name == "em0" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for common physical interface patterns
|
|
||||||
for _, prefix := range physicalPrefixes {
|
|
||||||
if strings.HasPrefix(strings.ToLower(name), prefix) {
|
|
||||||
// Check if the remaining part is numeric or empty
|
|
||||||
suffix := strings.TrimPrefix(strings.ToLower(name), prefix)
|
|
||||||
if suffix == "" || isNumericSuffix(suffix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNumericSuffix checks if a string is a numeric suffix or starts with a number
|
|
||||||
func isNumericSuffix(s string) bool {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the first character is a digit
|
|
||||||
if s[0] >= '0' && s[0] <= '9' {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isRealExternalIP checks if an IP is a genuine external (public) IP
|
|
||||||
func isRealExternalIP(ip stdnet.IP, ipNet *stdnet.IPNet) bool {
|
|
||||||
// Skip if it's not a global unicast address
|
|
||||||
if !ip.IsGlobalUnicast() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip private IPs
|
|
||||||
if ip.IsPrivate() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip link-local addresses
|
|
||||||
if ip.IsLinkLocalUnicast() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip loopback
|
|
||||||
if ip.IsLoopback() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip multicast
|
|
||||||
if ip.IsMulticast() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for special reserved ranges
|
|
||||||
if isReservedIP(ip) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// isReservedIP checks if an IP belongs to special reserved ranges
|
// isReservedIP checks if an IP belongs to special reserved ranges
|
||||||
func isReservedIP(ip stdnet.IP) bool {
|
func isReservedIP(ip stdnet.IP) bool {
|
||||||
// Handle IPv4
|
// Handle IPv4
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package analytic
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdnet "net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustCIDR(t *testing.T, value string) stdnet.Addr {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ip, ipNet, err := stdnet.ParseCIDR(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse CIDR %q: %v", value, err)
|
||||||
|
}
|
||||||
|
ipNet.IP = ip
|
||||||
|
return ipNet
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceAcceptsWindowsEthernetPrivateIPv4(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "Ethernet0",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0x50, 0x56, 0xba, 0x25, 0x01},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.100.1.10/28")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected Windows Ethernet interface with private IPv4 to be counted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceRejectsTapAdapter(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "TAP-Windows Adapter V9",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0xff, 0x55, 0x61, 0x3a, 0xd2},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.8.0.2/24")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected TAP adapter to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceRejectsLinkLocalOnly(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "Ethernet0",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0x50, 0x56, 0xba, 0x25, 0x01},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "fe80::c562:f8dc:9cd4:18eb/64")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected link-local-only interface to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCountNetworkInterfaceRejectsLoopback(t *testing.T) {
|
||||||
|
iface := networkInterfaceInfo{
|
||||||
|
Name: "Loopback Pseudo-Interface 1",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagLoopback,
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "127.0.0.1/8")},
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCountNetworkInterface(iface) {
|
||||||
|
t.Fatalf("expected loopback interface to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCountedInterfaceSetIncludesOnlyEligibleNames(t *testing.T) {
|
||||||
|
interfaces := []networkInterfaceInfo{
|
||||||
|
{
|
||||||
|
Name: "Ethernet0",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0x50, 0x56, 0xba, 0x25, 0x01},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.100.1.10/28")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TAP-Windows Adapter V9",
|
||||||
|
Flags: stdnet.FlagUp | stdnet.FlagBroadcast | stdnet.FlagMulticast,
|
||||||
|
HardwareAddr: stdnet.HardwareAddr{0x00, 0xff, 0x55, 0x61, 0x3a, 0xd2},
|
||||||
|
Addrs: []stdnet.Addr{mustCIDR(t, "10.8.0.2/24")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
counted := buildCountedInterfaceSet(interfaces)
|
||||||
|
if !counted["Ethernet0"] {
|
||||||
|
t.Fatalf("expected Ethernet0 to be counted")
|
||||||
|
}
|
||||||
|
if counted["TAP-Windows Adapter V9"] {
|
||||||
|
t.Fatalf("expected TAP adapter to be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user