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 (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -72,17 +76,13 @@ func Scan(ifaces, args string, strs []string) []models.Host {
|
||||||
var foundHosts = []models.Host{}
|
var foundHosts = []models.Host{}
|
||||||
arpArgs = args
|
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, " ")
|
foundHosts = append(foundHosts, parseOutput(text, iface)...)
|
||||||
|
|
||||||
for _, iface := range p {
|
|
||||||
slog.Debug("Scanning interface " + iface)
|
|
||||||
text = scanIface(iface)
|
|
||||||
slog.Debug("Found IPs: \n" + text)
|
|
||||||
|
|
||||||
foundHosts = append(foundHosts, parseOutput(text, iface)...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range strs {
|
for _, s := range strs {
|
||||||
|
|
@ -96,3 +96,192 @@ func Scan(ifaces, args string, strs []string) []models.Host {
|
||||||
|
|
||||||
return foundHosts
|
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