Auto-select usable scan interfaces
This commit is contained in:
parent
1c223859df
commit
d92b0f6b0e
2 changed files with 268 additions and 10 deletions
|
|
@ -2,7 +2,11 @@ package arp
|
|||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -72,17 +76,13 @@ func Scan(ifaces, args string, strs []string) []models.Host {
|
|||
var foundHosts = []models.Host{}
|
||||
arpArgs = args
|
||||
|
||||
if ifaces != "" {
|
||||
p = resolveScanInterfaces(ifaces)
|
||||
for _, iface := range p {
|
||||
slog.Debug("Scanning interface " + iface)
|
||||
text = scanIface(iface)
|
||||
slog.Debug("Found IPs: \n" + text)
|
||||
|
||||
p = strings.Split(ifaces, " ")
|
||||
|
||||
for _, iface := range p {
|
||||
slog.Debug("Scanning interface " + iface)
|
||||
text = scanIface(iface)
|
||||
slog.Debug("Found IPs: \n" + text)
|
||||
|
||||
foundHosts = append(foundHosts, parseOutput(text, iface)...)
|
||||
}
|
||||
foundHosts = append(foundHosts, parseOutput(text, iface)...)
|
||||
}
|
||||
|
||||
for _, s := range strs {
|
||||
|
|
@ -96,3 +96,192 @@ func Scan(ifaces, args string, strs []string) []models.Host {
|
|||
|
||||
return foundHosts
|
||||
}
|
||||
|
||||
func resolveScanInterfaces(ifaces string) []string {
|
||||
configured := strings.Fields(ifaces)
|
||||
available := usableInterfaceMap()
|
||||
auto := autoScanInterfaces(available)
|
||||
selected, invalid, usedAuto := selectScanInterfaces(configured, available, auto)
|
||||
|
||||
if len(invalid) > 0 {
|
||||
slog.Warn("Ignoring unavailable scan interfaces", "ifaces", strings.Join(invalid, " "))
|
||||
}
|
||||
if usedAuto && len(selected) > 0 {
|
||||
slog.Warn("Using auto-detected scan interfaces", "ifaces", strings.Join(selected, " "))
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
func selectScanInterfaces(configured []string, available map[string]bool, auto []string) ([]string, []string, bool) {
|
||||
if len(configured) == 0 {
|
||||
return uniqueStrings(auto), nil, len(auto) > 0
|
||||
}
|
||||
|
||||
var selected []string
|
||||
var invalid []string
|
||||
for _, iface := range configured {
|
||||
if available[iface] {
|
||||
selected = appendUniqueString(selected, iface)
|
||||
continue
|
||||
}
|
||||
invalid = appendUniqueString(invalid, iface)
|
||||
}
|
||||
|
||||
if len(selected) > 0 {
|
||||
return selected, invalid, false
|
||||
}
|
||||
|
||||
return uniqueStrings(auto), invalid, len(auto) > 0
|
||||
}
|
||||
|
||||
func usableInterfaceMap() map[string]bool {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
slog.Warn("Cannot list network interfaces", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
available := make(map[string]bool)
|
||||
for _, iface := range interfaces {
|
||||
if isUsableInterface(iface) {
|
||||
available[iface.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
func isUsableInterface(iface net.Interface) bool {
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
slog.Debug("Cannot list interface addresses", "iface", iface.Name, "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if hasUsableIPv4(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func hasUsableIPv4(addr net.Addr) bool {
|
||||
ipNet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
ip := ipNet.IP.To4()
|
||||
return ip != nil && !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
|
||||
}
|
||||
|
||||
func autoScanInterfaces(available map[string]bool) []string {
|
||||
if len(available) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ifaces := defaultRouteInterfaces(available)
|
||||
if len(ifaces) > 0 {
|
||||
return ifaces
|
||||
}
|
||||
|
||||
for iface := range available {
|
||||
if isVirtualInterface(iface) {
|
||||
continue
|
||||
}
|
||||
ifaces = append(ifaces, iface)
|
||||
}
|
||||
sort.Strings(ifaces)
|
||||
return ifaces
|
||||
}
|
||||
|
||||
func defaultRouteInterfaces(available map[string]bool) []string {
|
||||
data, err := os.ReadFile("/proc/net/route")
|
||||
if err != nil {
|
||||
slog.Debug("Cannot read default routes", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var ifaces []string
|
||||
minMetric := -1
|
||||
for _, line := range strings.Split(string(data), "\n")[1:] {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 7 || fields[1] != "00000000" {
|
||||
continue
|
||||
}
|
||||
|
||||
iface := fields[0]
|
||||
if !available[iface] || isVirtualInterface(iface) {
|
||||
continue
|
||||
}
|
||||
|
||||
metric, err := strconv.Atoi(fields[6])
|
||||
if err != nil {
|
||||
metric = 0
|
||||
}
|
||||
|
||||
switch {
|
||||
case minMetric == -1 || metric < minMetric:
|
||||
minMetric = metric
|
||||
ifaces = []string{iface}
|
||||
case metric == minMetric:
|
||||
ifaces = appendUniqueString(ifaces, iface)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(ifaces)
|
||||
return ifaces
|
||||
}
|
||||
|
||||
func isVirtualInterface(iface string) bool {
|
||||
prefixes := []string{
|
||||
"br-",
|
||||
"docker",
|
||||
"lxc",
|
||||
"tailscale",
|
||||
"tap",
|
||||
"tun",
|
||||
"vboxnet",
|
||||
"veth",
|
||||
"virbr",
|
||||
"vmnet",
|
||||
"wg",
|
||||
}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(iface, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func uniqueStrings(values []string) []string {
|
||||
var out []string
|
||||
for _, value := range values {
|
||||
out = appendUniqueString(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func appendUniqueString(values []string, value string) []string {
|
||||
if value == "" {
|
||||
return values
|
||||
}
|
||||
|
||||
for _, item := range values {
|
||||
if item == value {
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
return append(values, value)
|
||||
}
|
||||
|
|
|
|||
69
backend/internal/arp/arpscan_test.go
Normal file
69
backend/internal/arp/arpscan_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package arp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSelectScanInterfacesKeepsValidConfiguredInterfaces(t *testing.T) {
|
||||
available := map[string]bool{
|
||||
"enp6s0": true,
|
||||
"wlo1": true,
|
||||
}
|
||||
|
||||
selected, invalid, usedAuto := selectScanInterfaces(
|
||||
[]string{"enp6s0", "missing0"},
|
||||
available,
|
||||
[]string{"wlo1"},
|
||||
)
|
||||
|
||||
if !reflect.DeepEqual(selected, []string{"enp6s0"}) {
|
||||
t.Fatalf("selected = %#v, want enp6s0", selected)
|
||||
}
|
||||
if !reflect.DeepEqual(invalid, []string{"missing0"}) {
|
||||
t.Fatalf("invalid = %#v, want missing0", invalid)
|
||||
}
|
||||
if usedAuto {
|
||||
t.Fatal("usedAuto = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectScanInterfacesFallsBackWhenAllConfiguredInterfacesAreInvalid(t *testing.T) {
|
||||
available := map[string]bool{
|
||||
"enp6s0": true,
|
||||
}
|
||||
|
||||
selected, invalid, usedAuto := selectScanInterfaces(
|
||||
[]string{"enP8p1s0"},
|
||||
available,
|
||||
[]string{"enp6s0"},
|
||||
)
|
||||
|
||||
if !reflect.DeepEqual(selected, []string{"enp6s0"}) {
|
||||
t.Fatalf("selected = %#v, want enp6s0", selected)
|
||||
}
|
||||
if !reflect.DeepEqual(invalid, []string{"enP8p1s0"}) {
|
||||
t.Fatalf("invalid = %#v, want enP8p1s0", invalid)
|
||||
}
|
||||
if !usedAuto {
|
||||
t.Fatal("usedAuto = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectScanInterfacesUsesAutoWhenConfigIsBlank(t *testing.T) {
|
||||
selected, invalid, usedAuto := selectScanInterfaces(
|
||||
nil,
|
||||
map[string]bool{"enp6s0": true},
|
||||
[]string{"enp6s0"},
|
||||
)
|
||||
|
||||
if !reflect.DeepEqual(selected, []string{"enp6s0"}) {
|
||||
t.Fatalf("selected = %#v, want enp6s0", selected)
|
||||
}
|
||||
if len(invalid) != 0 {
|
||||
t.Fatalf("invalid = %#v, want empty", invalid)
|
||||
}
|
||||
if !usedAuto {
|
||||
t.Fatal("usedAuto = false, want true")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue