Skip to content

Commit 50f4331

Browse files
committed
feat: collect and store IPv4 and IPv6 addresses in SystemInfo
1 parent cd97991 commit 50f4331

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

internal/metrics/ip.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package metrics
2+
3+
import (
4+
"bufio"
5+
"net"
6+
"os"
7+
"strings"
8+
9+
"github.com/node-pulse/agent/internal/logger"
10+
)
11+
12+
// collectIPAddresses collects IPv4 and IPv6 addresses for the server
13+
// Returns (ipv4, ipv6) where either can be empty string if not available
14+
func collectIPAddresses() (string, string) {
15+
var ipv4, ipv6 string
16+
17+
// Try method 1: Parse network interfaces
18+
if v4, v6 := getIPFromInterfaces(); v4 != "" || v6 != "" {
19+
ipv4, ipv6 = v4, v6
20+
return ipv4, ipv6
21+
}
22+
23+
// Try method 2: Parse /proc/net/route and /proc/net/ipv6_route for default routes
24+
if v4, v6 := getIPFromRoutes(); v4 != "" || v6 != "" {
25+
ipv4, ipv6 = v4, v6
26+
return ipv4, ipv6
27+
}
28+
29+
logger.Debug("Could not determine IP addresses from any method")
30+
return ipv4, ipv6
31+
}
32+
33+
// getIPFromInterfaces gets the IP addresses from network interfaces
34+
func getIPFromInterfaces() (string, string) {
35+
var ipv4, ipv6 string
36+
37+
ifaces, err := net.Interfaces()
38+
if err != nil {
39+
logger.Debug("Failed to get network interfaces", logger.Err(err))
40+
return ipv4, ipv6
41+
}
42+
43+
// Prioritize non-loopback, up interfaces
44+
for _, iface := range ifaces {
45+
// Skip down interfaces and loopback
46+
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
47+
continue
48+
}
49+
50+
addrs, err := iface.Addrs()
51+
if err != nil {
52+
continue
53+
}
54+
55+
for _, addr := range addrs {
56+
var ip net.IP
57+
switch v := addr.(type) {
58+
case *net.IPNet:
59+
ip = v.IP
60+
case *net.IPAddr:
61+
ip = v.IP
62+
default:
63+
continue
64+
}
65+
66+
// Skip loopback addresses
67+
if ip.IsLoopback() {
68+
continue
69+
}
70+
71+
// Check if IPv4
72+
if ip.To4() != nil && ipv4 == "" {
73+
ipv4 = ip.String()
74+
}
75+
76+
// Check if IPv6 (and not IPv4-mapped)
77+
if ip.To4() == nil && ipv6 == "" {
78+
// Skip link-local addresses (fe80::/10)
79+
if !ip.IsLinkLocalUnicast() {
80+
ipv6 = ip.String()
81+
}
82+
}
83+
84+
// If we found both, we're done
85+
if ipv4 != "" && ipv6 != "" {
86+
return ipv4, ipv6
87+
}
88+
}
89+
}
90+
91+
return ipv4, ipv6
92+
}
93+
94+
// getIPFromRoutes gets IP addresses by finding default route interfaces
95+
func getIPFromRoutes() (string, string) {
96+
var ipv4, ipv6 string
97+
98+
// Get IPv4 from default route
99+
if iface := getDefaultRouteInterface("/proc/net/route"); iface != "" {
100+
ipv4 = getInterfaceIP(iface, false)
101+
}
102+
103+
// Get IPv6 from default route
104+
if iface := getDefaultIPv6RouteInterface(); iface != "" {
105+
ipv6 = getInterfaceIP(iface, true)
106+
}
107+
108+
return ipv4, ipv6
109+
}
110+
111+
// getDefaultRouteInterface finds the interface name for the default route from /proc/net/route
112+
func getDefaultRouteInterface(path string) string {
113+
file, err := os.Open(path)
114+
if err != nil {
115+
return ""
116+
}
117+
defer file.Close()
118+
119+
scanner := bufio.NewScanner(file)
120+
// Skip header
121+
scanner.Scan()
122+
123+
for scanner.Scan() {
124+
fields := strings.Fields(scanner.Text())
125+
if len(fields) < 8 {
126+
continue
127+
}
128+
129+
// Check if destination is 00000000 (default route)
130+
if fields[1] == "00000000" {
131+
return fields[0] // Interface name
132+
}
133+
}
134+
135+
return ""
136+
}
137+
138+
// getDefaultIPv6RouteInterface finds the interface name for the default IPv6 route
139+
func getDefaultIPv6RouteInterface() string {
140+
file, err := os.Open("/proc/net/ipv6_route")
141+
if err != nil {
142+
return ""
143+
}
144+
defer file.Close()
145+
146+
scanner := bufio.NewScanner(file)
147+
148+
for scanner.Scan() {
149+
fields := strings.Fields(scanner.Text())
150+
if len(fields) < 10 {
151+
continue
152+
}
153+
154+
// Check if destination is 00000000000000000000000000000000 (default route)
155+
if fields[0] == "00000000000000000000000000000000" {
156+
return fields[9] // Interface name
157+
}
158+
}
159+
160+
return ""
161+
}
162+
163+
// getInterfaceIP gets the IP address for a specific interface
164+
func getInterfaceIP(ifaceName string, ipv6 bool) string {
165+
iface, err := net.InterfaceByName(ifaceName)
166+
if err != nil {
167+
return ""
168+
}
169+
170+
addrs, err := iface.Addrs()
171+
if err != nil {
172+
return ""
173+
}
174+
175+
for _, addr := range addrs {
176+
var ip net.IP
177+
switch v := addr.(type) {
178+
case *net.IPNet:
179+
ip = v.IP
180+
case *net.IPAddr:
181+
ip = v.IP
182+
default:
183+
continue
184+
}
185+
186+
if ipv6 {
187+
// Looking for IPv6
188+
if ip.To4() == nil && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() {
189+
return ip.String()
190+
}
191+
} else {
192+
// Looking for IPv4
193+
if ip.To4() != nil && !ip.IsLoopback() {
194+
return ip.String()
195+
}
196+
}
197+
}
198+
199+
return ""
200+
}

internal/metrics/system.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type SystemInfo struct {
1616
DistroVer string `json:"distro_version"`
1717
Architecture string `json:"architecture"`
1818
CPUCores int `json:"cpu_cores"`
19+
IPv4 string `json:"ipv4,omitempty"`
20+
IPv6 string `json:"ipv6,omitempty"`
1921
}
2022

2123
var cachedSystemInfo *SystemInfo
@@ -51,6 +53,11 @@ func CollectSystemInfo() (*SystemInfo, error) {
5153
info.DistroVer = version
5254
}
5355

56+
// Get IP addresses (optional - may be empty if not available)
57+
ipv4, ipv6 := collectIPAddresses()
58+
info.IPv4 = ipv4
59+
info.IPv6 = ipv6
60+
5461
cachedSystemInfo = info
5562
return info, nil
5663
}

0 commit comments

Comments
 (0)