Discovery Fixes: - Always update cache even when scan finds no servers (prevents stale data) - Remove automatic re-add of deleted nodes to discovery (was causing confusion) - Optimize Docker subnet scanning from 762 IPs to 254 IPs (3x faster) - Add getHostSubnetFromGateway() to detect host network from container Frontend Type Fixes: - Fix ThresholdsTable editScope type errors - Fix SnapshotAlertConfig index signature - Remove unused variable in Settings.tsx These changes make discovery faster, more reliable, and fix the issue where deleted nodes would persist in the discovery cache or immediately reappear.
731 lines
20 KiB
Go
731 lines
20 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// DiscoveredServer represents a discovered Proxmox/PBS/PMG server
|
|
type DiscoveredServer struct {
|
|
IP string `json:"ip"`
|
|
Port int `json:"port"`
|
|
Type string `json:"type"` // "pve", "pbs", or "pmg"
|
|
Version string `json:"version"`
|
|
Hostname string `json:"hostname,omitempty"`
|
|
Release string `json:"release,omitempty"`
|
|
}
|
|
|
|
// DiscoveryResult contains all discovered servers
|
|
type DiscoveryResult struct {
|
|
Servers []DiscoveredServer `json:"servers"`
|
|
Errors []string `json:"errors,omitempty"`
|
|
}
|
|
|
|
// Scanner handles network scanning for Proxmox/PBS servers
|
|
type Scanner struct {
|
|
timeout time.Duration
|
|
concurrent int
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewScanner creates a new network scanner
|
|
func NewScanner() *Scanner {
|
|
return &Scanner{
|
|
timeout: 1 * time.Second, // Reduced timeout for faster scanning
|
|
concurrent: 50, // Increased concurrent workers for faster scanning
|
|
httpClient: &http.Client{
|
|
Timeout: 2 * time.Second, // Reduced HTTP timeout
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
MaxIdleConns: 100,
|
|
MaxConnsPerHost: 10,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ServerCallback is called when a server is discovered
|
|
type ServerCallback func(server DiscoveredServer)
|
|
|
|
// DiscoverServers scans the network for Proxmox VE and PBS servers
|
|
func (s *Scanner) DiscoverServers(ctx context.Context, subnet string) (*DiscoveryResult, error) {
|
|
return s.DiscoverServersWithCallback(ctx, subnet, nil)
|
|
}
|
|
|
|
// DiscoverServersWithCallback scans and calls callback for each discovered server
|
|
func (s *Scanner) DiscoverServersWithCallback(ctx context.Context, subnet string, callback ServerCallback) (*DiscoveryResult, error) {
|
|
log.Info().Str("subnet", subnet).Msg("Starting network discovery")
|
|
|
|
// Parse subnet
|
|
var ipNets []*net.IPNet
|
|
|
|
if subnet == "" || subnet == "auto" {
|
|
// Check if we're in Docker (detected subnet is Docker network)
|
|
autoDetected := s.getLocalSubnet()
|
|
if autoDetected != nil && (strings.HasPrefix(autoDetected.String(), "172.17.") || strings.HasPrefix(autoDetected.String(), "172.1")) {
|
|
log.Info().Msg("Running in Docker - detecting host network from gateway")
|
|
// Try to detect the host's network from the default gateway
|
|
if hostSubnet := s.getHostSubnetFromGateway(); hostSubnet != nil {
|
|
ipNets = []*net.IPNet{hostSubnet}
|
|
log.Info().Str("detected", hostSubnet.String()).Msg("Detected host subnet from Docker gateway")
|
|
} else {
|
|
// Fallback: scan only most common subnet (192.168.0.0/24)
|
|
log.Info().Msg("Could not detect host subnet - scanning 192.168.0.0/24")
|
|
_, defaultNet, _ := net.ParseCIDR("192.168.0.0/24")
|
|
ipNets = []*net.IPNet{defaultNet}
|
|
}
|
|
} else if autoDetected != nil {
|
|
// Use auto-detected subnet
|
|
ipNets = []*net.IPNet{autoDetected}
|
|
log.Info().Str("detected", autoDetected.String()).Msg("Auto-detected local subnet")
|
|
} else {
|
|
// Fallback to most common subnet
|
|
log.Info().Msg("Auto-detection failed - scanning 192.168.0.0/24")
|
|
_, defaultNet, _ := net.ParseCIDR("192.168.0.0/24")
|
|
ipNets = []*net.IPNet{defaultNet}
|
|
}
|
|
} else {
|
|
// Parse provided subnet
|
|
_, parsedNet, err := net.ParseCIDR(subnet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid subnet: %w", err)
|
|
}
|
|
ipNets = []*net.IPNet{parsedNet}
|
|
}
|
|
|
|
// Collect all IPs to scan from all subnets
|
|
var allIPs []string
|
|
for _, ipNet := range ipNets {
|
|
// Check subnet size - limit to /24 or smaller for safety
|
|
ones, bits := ipNet.Mask.Size()
|
|
if ones < 24 && bits == 32 { // IPv4 with more than 256 addresses
|
|
log.Warn().Str("subnet", ipNet.String()).Msg("Subnet too large, limiting to /24")
|
|
// Convert to /24
|
|
ipNet.Mask = net.CIDRMask(24, 32)
|
|
}
|
|
|
|
// Generate list of IPs for this subnet
|
|
ips := s.generateIPs(ipNet)
|
|
allIPs = append(allIPs, ips...)
|
|
log.Info().Str("subnet", ipNet.String()).Int("count", len(ips)).Msg("Subnet IPs to scan")
|
|
}
|
|
log.Info().Int("total", len(allIPs)).Msg("Total IPs to scan")
|
|
|
|
// Create channels for work distribution
|
|
ipChan := make(chan string, len(allIPs))
|
|
resultChan := make(chan *DiscoveredServer, len(allIPs))
|
|
errorChan := make(chan string, len(allIPs))
|
|
|
|
// Start workers
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < s.concurrent; i++ {
|
|
wg.Add(1)
|
|
go s.scanWorker(ctx, &wg, ipChan, resultChan, errorChan)
|
|
}
|
|
|
|
// Send IPs to scan
|
|
for _, ip := range allIPs {
|
|
ipChan <- ip
|
|
}
|
|
close(ipChan)
|
|
|
|
// Wait for workers to finish
|
|
go func() {
|
|
wg.Wait()
|
|
close(resultChan)
|
|
close(errorChan)
|
|
}()
|
|
|
|
// Collect results
|
|
result := &DiscoveryResult{
|
|
Servers: []DiscoveredServer{},
|
|
Errors: []string{},
|
|
}
|
|
|
|
done := false
|
|
for !done {
|
|
select {
|
|
case server, ok := <-resultChan:
|
|
if !ok {
|
|
done = true
|
|
break
|
|
}
|
|
if server != nil {
|
|
result.Servers = append(result.Servers, *server)
|
|
// Log immediately when found for real-time feedback
|
|
log.Info().
|
|
Str("ip", server.IP).
|
|
Str("type", server.Type).
|
|
Str("hostname", server.Hostname).
|
|
Msg("🎯 Discovered server - adding to results")
|
|
|
|
// Call callback for real-time updates
|
|
if callback != nil {
|
|
callback(*server)
|
|
}
|
|
}
|
|
case errMsg, ok := <-errorChan:
|
|
if ok && errMsg != "" {
|
|
result.Errors = append(result.Errors, errMsg)
|
|
}
|
|
case <-ctx.Done():
|
|
return result, ctx.Err()
|
|
}
|
|
}
|
|
|
|
log.Info().
|
|
Int("found", len(result.Servers)).
|
|
Int("errors", len(result.Errors)).
|
|
Msg("Discovery completed")
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// scanWorker scans IPs from the channel
|
|
func (s *Scanner) scanWorker(ctx context.Context, wg *sync.WaitGroup, ipChan <-chan string, resultChan chan<- *DiscoveredServer, errorChan chan<- string) {
|
|
defer wg.Done()
|
|
|
|
for ip := range ipChan {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
if server := s.checkPort8006(ctx, ip); server != nil {
|
|
resultChan <- server
|
|
}
|
|
|
|
if server := s.checkServer(ctx, ip, 8007, "pbs"); server != nil {
|
|
resultChan <- server
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkPort8006 checks if port 8006 is running PMG or PVE
|
|
func (s *Scanner) checkPort8006(ctx context.Context, ip string) *DiscoveredServer {
|
|
address := net.JoinHostPort(ip, "8006")
|
|
|
|
// First attempt a TLS handshake so we can inspect certificate metadata.
|
|
var tlsState *tls.ConnectionState
|
|
dialer := &net.Dialer{Timeout: s.timeout}
|
|
tlsConn, tlsErr := tls.DialWithDialer(dialer, "tcp", address, &tls.Config{InsecureSkipVerify: true})
|
|
if tlsErr != nil {
|
|
// Fallback to a simple TCP dial to confirm the port is open.
|
|
conn, err := net.DialTimeout("tcp", address, s.timeout)
|
|
if err != nil {
|
|
return nil // Port not open
|
|
}
|
|
conn.Close()
|
|
} else {
|
|
state := tlsConn.ConnectionState()
|
|
tlsState = &state
|
|
tlsConn.Close()
|
|
}
|
|
|
|
serverType := "pve"
|
|
if tlsState != nil {
|
|
if guess := inferTypeFromCertificate(*tlsState); guess != "" {
|
|
serverType = guess
|
|
}
|
|
}
|
|
|
|
version := "Unknown"
|
|
var release string
|
|
|
|
// Try to get version without auth (some installations allow it)
|
|
versionURL := fmt.Sprintf("https://%s/api2/json/version", address)
|
|
if req, err := http.NewRequestWithContext(ctx, "GET", versionURL, nil); err == nil {
|
|
if resp, err := s.httpClient.Do(req); err == nil {
|
|
defer resp.Body.Close()
|
|
|
|
switch resp.StatusCode {
|
|
case http.StatusOK:
|
|
var versionResp struct {
|
|
Data struct {
|
|
Version string `json:"version"`
|
|
Release string `json:"release,omitempty"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&versionResp); err == nil && versionResp.Data.Version != "" {
|
|
version = versionResp.Data.Version
|
|
release = versionResp.Data.Release
|
|
|
|
if guess := inferTypeFromMetadata(
|
|
versionResp.Data.Version,
|
|
versionResp.Data.Release,
|
|
resp.Header.Get("Server"),
|
|
resp.Header.Get("Proxmox-Product"),
|
|
resp.Header.Get("WWW-Authenticate"),
|
|
strings.Join(resp.Header.Values("Set-Cookie"), " "),
|
|
); guess != "" {
|
|
serverType = guess
|
|
}
|
|
|
|
log.Info().
|
|
Str("ip", ip).
|
|
Int("port", 8006).
|
|
Str("version", version).
|
|
Msg("Got server version without auth")
|
|
}
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
if guess := inferTypeFromMetadata(
|
|
resp.Header.Get("WWW-Authenticate"),
|
|
resp.Header.Get("Server"),
|
|
resp.Header.Get("Proxmox-Product"),
|
|
); guess != "" {
|
|
serverType = guess
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: probe PMG-specific endpoints if we still think this is a PVE server.
|
|
if serverType != "pmg" && s.isPMGServer(ctx, address) {
|
|
serverType = "pmg"
|
|
}
|
|
|
|
log.Info().
|
|
Str("ip", ip).
|
|
Int("port", 8006).
|
|
Str("type", serverType).
|
|
Msg("Found potential server (port open)")
|
|
|
|
server := &DiscoveredServer{
|
|
IP: ip,
|
|
Port: 8006,
|
|
Type: serverType,
|
|
Version: version,
|
|
Release: release,
|
|
}
|
|
|
|
// Try to resolve hostname via reverse DNS
|
|
names, err := net.LookupAddr(ip)
|
|
if err == nil && len(names) > 0 {
|
|
// Use the first hostname, remove trailing dot if present
|
|
hostname := strings.TrimSuffix(names[0], ".")
|
|
server.Hostname = hostname
|
|
log.Debug().Str("ip", ip).Str("hostname", hostname).Msg("Resolved hostname via DNS")
|
|
}
|
|
|
|
return server
|
|
}
|
|
|
|
// isPMGServer checks if a server is PMG by checking for PMG-specific endpoints
|
|
func (s *Scanner) isPMGServer(ctx context.Context, address string) bool {
|
|
endpoints := []string{
|
|
"api2/json/statistics/mail",
|
|
"api2/json/mail/queue",
|
|
"api2/json/mail/quarantine",
|
|
}
|
|
|
|
for _, endpoint := range endpoints {
|
|
product := s.detectProductFromEndpoint(ctx, address, endpoint)
|
|
if product == "pmg" {
|
|
log.Debug().
|
|
Str("address", address).
|
|
Str("endpoint", endpoint).
|
|
Msg("PMG-specific endpoint confirmed")
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// detectProductFromEndpoint inspects an HTTP endpoint and tries to infer the product type.
|
|
func (s *Scanner) detectProductFromEndpoint(ctx context.Context, address, endpoint string) string {
|
|
url := fmt.Sprintf("https://%s/%s", address, endpoint)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
headerProduct := inferTypeFromMetadata(
|
|
resp.Header.Get("Server"),
|
|
resp.Header.Get("Proxmox-Product"),
|
|
resp.Header.Get("WWW-Authenticate"),
|
|
strings.Join(resp.Header.Values("Set-Cookie"), " "),
|
|
)
|
|
if headerProduct != "" {
|
|
return headerProduct
|
|
}
|
|
|
|
// If the endpoint responded (not 404) and the path is PMG-specific, treat it as PMG.
|
|
if resp.StatusCode != http.StatusNotFound && strings.Contains(endpoint, "mail") {
|
|
return "pmg"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// inferTypeFromCertificate tries to determine the product based on TLS certificate metadata.
|
|
func inferTypeFromCertificate(state tls.ConnectionState) string {
|
|
if len(state.PeerCertificates) == 0 {
|
|
return ""
|
|
}
|
|
|
|
cert := state.PeerCertificates[0]
|
|
parts := []string{cert.Subject.CommonName}
|
|
parts = append(parts, cert.Subject.Organization...)
|
|
parts = append(parts, cert.Subject.OrganizationalUnit...)
|
|
|
|
return inferTypeFromMetadata(parts...)
|
|
}
|
|
|
|
// inferTypeFromMetadata inspects textual metadata and returns a best-effort product type.
|
|
func inferTypeFromMetadata(parts ...string) string {
|
|
var builder strings.Builder
|
|
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
if builder.Len() > 0 {
|
|
builder.WriteByte(' ')
|
|
}
|
|
builder.WriteString(strings.ToLower(part))
|
|
}
|
|
|
|
combined := builder.String()
|
|
if combined == "" {
|
|
return ""
|
|
}
|
|
|
|
compact := strings.ReplaceAll(combined, " ", "")
|
|
|
|
switch {
|
|
case strings.Contains(combined, "pmg"),
|
|
strings.Contains(combined, "mail gateway"),
|
|
strings.Contains(combined, "pmgauth"),
|
|
strings.Contains(combined, "pmgauthcookie"),
|
|
strings.Contains(compact, "mailgateway"),
|
|
strings.Contains(compact, "pmg-api"):
|
|
return "pmg"
|
|
case strings.Contains(combined, "pbs"),
|
|
strings.Contains(combined, "backup server"),
|
|
strings.Contains(combined, "pbsauth"),
|
|
strings.Contains(compact, "pbs-api"):
|
|
return "pbs"
|
|
case strings.Contains(combined, "pve"),
|
|
strings.Contains(combined, "virtual environment"),
|
|
strings.Contains(combined, "pveauth"),
|
|
strings.Contains(compact, "pve-api"):
|
|
return "pve"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// checkServer checks if a server is running at the given IP and port
|
|
func (s *Scanner) checkServer(ctx context.Context, ip string, port int, serverType string) *DiscoveredServer {
|
|
// First check if port is open
|
|
address := net.JoinHostPort(ip, strconv.Itoa(port))
|
|
conn, err := net.DialTimeout("tcp", address, s.timeout)
|
|
if err != nil {
|
|
return nil // Port not open
|
|
}
|
|
conn.Close()
|
|
|
|
// Port is open - this is likely a Proxmox/PBS server
|
|
// Since most installations require auth for version endpoint,
|
|
// we'll return it as a discovered server based on the port alone
|
|
|
|
log.Info().
|
|
Str("ip", ip).
|
|
Int("port", port).
|
|
Str("type", serverType).
|
|
Msg("Found potential server (port open)")
|
|
|
|
server := &DiscoveredServer{
|
|
IP: ip,
|
|
Port: port,
|
|
Type: serverType,
|
|
Version: "Unknown", // Will be determined after auth
|
|
}
|
|
|
|
// Try to get version without auth (some installations allow it)
|
|
url := fmt.Sprintf("https://%s/api2/json/version", address)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err == nil {
|
|
resp, err := s.httpClient.Do(req)
|
|
if err == nil {
|
|
defer resp.Body.Close()
|
|
|
|
// Only try to parse if we got a successful response
|
|
if resp.StatusCode == 200 {
|
|
var versionResp struct {
|
|
Data struct {
|
|
Version string `json:"version"`
|
|
Release string `json:"release,omitempty"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&versionResp); err == nil && versionResp.Data.Version != "" {
|
|
server.Version = versionResp.Data.Version
|
|
server.Release = versionResp.Data.Release
|
|
|
|
log.Info().
|
|
Str("ip", ip).
|
|
Int("port", port).
|
|
Str("version", server.Version).
|
|
Msg("Got server version without auth")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to resolve hostname via reverse DNS
|
|
names, err := net.LookupAddr(ip)
|
|
if err == nil && len(names) > 0 {
|
|
// Use the first hostname, remove trailing dot if present
|
|
hostname := strings.TrimSuffix(names[0], ".")
|
|
server.Hostname = hostname
|
|
log.Debug().Str("ip", ip).Str("hostname", hostname).Msg("Resolved hostname via DNS")
|
|
}
|
|
|
|
return server
|
|
}
|
|
|
|
// getProxmoxHostname tries to get the hostname of a Proxmox VE server
|
|
func (s *Scanner) getProxmoxHostname(ctx context.Context, ip string, port int) string {
|
|
address := net.JoinHostPort(ip, strconv.Itoa(port))
|
|
url := fmt.Sprintf("https://%s/api2/json/nodes", address)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var nodesResp struct {
|
|
Data []struct {
|
|
Node string `json:"node"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&nodesResp); err != nil {
|
|
return ""
|
|
}
|
|
|
|
if len(nodesResp.Data) > 0 {
|
|
return nodesResp.Data[0].Node
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// getPBSHostname tries to get the hostname of a PBS server
|
|
func (s *Scanner) getPBSHostname(ctx context.Context, ip string, port int) string {
|
|
address := net.JoinHostPort(ip, strconv.Itoa(port))
|
|
url := fmt.Sprintf("https://%s/api2/json/nodes", address)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var nodesResp struct {
|
|
Data []struct {
|
|
Node string `json:"node"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&nodesResp); err != nil {
|
|
return ""
|
|
}
|
|
|
|
if len(nodesResp.Data) > 0 {
|
|
return nodesResp.Data[0].Node
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// generateIPs generates all IPs in a subnet
|
|
func (s *Scanner) generateIPs(ipNet *net.IPNet) []string {
|
|
var ips []string
|
|
|
|
// Get the starting IP
|
|
ip := ipNet.IP.Mask(ipNet.Mask)
|
|
|
|
// Calculate the number of hosts
|
|
ones, bits := ipNet.Mask.Size()
|
|
hostBits := bits - ones
|
|
numHosts := 1 << hostBits
|
|
|
|
// Skip network and broadcast addresses for common subnets
|
|
start := 1
|
|
end := numHosts - 1
|
|
if numHosts > 256 {
|
|
// For larger subnets, scan everything
|
|
start = 0
|
|
end = numHosts
|
|
}
|
|
|
|
// Limit to maximum 1024 IPs to avoid scanning huge networks
|
|
if end-start > 1024 {
|
|
end = start + 1024
|
|
log.Warn().Int("limited_to", 1024).Msg("Limiting scan to first 1024 IPs")
|
|
}
|
|
|
|
for i := start; i < end; i++ {
|
|
// Calculate IP
|
|
currIP := make(net.IP, len(ip))
|
|
copy(currIP, ip)
|
|
|
|
// Add offset to IP address
|
|
offset := i
|
|
for j := len(currIP) - 1; j >= 0 && offset > 0; j-- {
|
|
currIP[j] += byte(offset & 0xFF)
|
|
offset >>= 8
|
|
}
|
|
|
|
// Skip common non-server IPs
|
|
lastOctet := currIP[len(currIP)-1]
|
|
if lastOctet == 0 || lastOctet == 255 {
|
|
continue // Skip network and broadcast
|
|
}
|
|
|
|
ips = append(ips, currIP.String())
|
|
}
|
|
|
|
return ips
|
|
}
|
|
|
|
// getLocalSubnet attempts to detect the local subnet
|
|
func (s *Scanner) getLocalSubnet() *net.IPNet {
|
|
interfaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
for _, iface := range interfaces {
|
|
// Skip loopback and down interfaces
|
|
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
|
|
continue
|
|
}
|
|
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
if ipNet, ok := addr.(*net.IPNet); ok && ipNet.IP.To4() != nil {
|
|
// Found an IPv4 address
|
|
if !ipNet.IP.IsLoopback() && !ipNet.IP.IsLinkLocalUnicast() {
|
|
// Convert to /24 subnet for auto-detection
|
|
// This ensures we scan a reasonable range
|
|
ip := ipNet.IP.To4()
|
|
if ip != nil {
|
|
// Create a /24 subnet from the IP
|
|
ip[3] = 0 // Set last octet to 0
|
|
return &net.IPNet{
|
|
IP: ip,
|
|
Mask: net.CIDRMask(24, 32),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default to common subnet if detection fails
|
|
_, defaultNet, _ := net.ParseCIDR("192.168.1.0/24")
|
|
return defaultNet
|
|
}
|
|
|
|
// getHostSubnetFromGateway detects the host network by examining the default gateway
|
|
// This is useful when running in Docker to detect the actual host's network
|
|
func (s *Scanner) getHostSubnetFromGateway() *net.IPNet {
|
|
interfaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Find the default gateway by checking routes
|
|
// In Docker, the gateway IP usually ends in .1 and is on the Docker bridge network
|
|
for _, iface := range interfaces {
|
|
if iface.Flags&net.FlagUp == 0 {
|
|
continue
|
|
}
|
|
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
if ipNet, ok := addr.(*net.IPNet); ok && ipNet.IP.To4() != nil {
|
|
// Check if this looks like a Docker bridge network (172.17.x.x or similar)
|
|
ipStr := ipNet.IP.String()
|
|
if strings.HasPrefix(ipStr, "172.17.") || strings.HasPrefix(ipStr, "172.1") {
|
|
// Gateway is typically .1 in the same subnet
|
|
// Try to derive the host network: gateway .1 -> likely host is 192.168.x.0/24
|
|
// We'll try the .1 address as the gateway and ping common host subnets
|
|
|
|
// For now, just return the most common subnet
|
|
// A more sophisticated approach would parse /proc/net/route
|
|
_, hostNet, _ := net.ParseCIDR("192.168.0.0/24")
|
|
return hostNet
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getCommonSubnets returns a list of common home/office network subnets
|
|
func (s *Scanner) getCommonSubnets() []*net.IPNet {
|
|
// Ordered by likelihood - most common first for faster results
|
|
commonSubnets := []string{
|
|
"192.168.1.0/24", // Most common home router default
|
|
"192.168.0.0/24", // Very common alternative
|
|
"10.0.0.0/24", // Some routers use this
|
|
// Skip less common ones for speed:
|
|
// "192.168.88.0/24", // MikroTik default (uncommon)
|
|
// "172.16.0.0/24", // Less common but used
|
|
}
|
|
|
|
var nets []*net.IPNet
|
|
for _, subnet := range commonSubnets {
|
|
_, ipNet, err := net.ParseCIDR(subnet)
|
|
if err == nil {
|
|
nets = append(nets, ipNet)
|
|
}
|
|
}
|
|
|
|
return nets
|
|
}
|