Auto-select usable scan interfaces

This commit is contained in:
Corsair-cxs 2026-06-20 17:54:21 +08:00
parent 1c223859df
commit d92b0f6b0e
2 changed files with 268 additions and 10 deletions

View file

@ -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)
}

View 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")
}
}