refactor: consolidate network interface detection logic

Move platform-specific network interface detection from cmd/cli/ to root package
as ValidInterfaces function. This eliminates code duplication and provides a
consistent interface for determining valid physical network interfaces across
all platforms.

- Remove duplicate validInterfacesMap functions from platform-specific files
- Add context parameter to virtualInterfaces for proper logging
- Update all callers to use ctrld.ValidInterfaces instead of local functions
- Improve error handling in virtual interface detection on Linux
This commit is contained in:
Cuong Manh Le
2025-10-01 16:46:28 +07:00
committed by Cuong Manh Le
parent f24059885f
commit 650e47a504
12 changed files with 36 additions and 141 deletions
+1 -1
View File
@@ -1435,7 +1435,7 @@ func (p *prog) monitorNetworkChanges(ctx context.Context) error {
mon.RegisterChangeCallback(func(delta *netmon.ChangeDelta) { mon.RegisterChangeCallback(func(delta *netmon.ChangeDelta) {
// Get map of valid interfaces // Get map of valid interfaces
validIfaces := validInterfacesMap(ctrld.LoggerCtx(ctx, p.logger.Load())) validIfaces := ctrld.ValidInterfaces(ctrld.LoggerCtx(ctx, p.logger.Load()))
isMajorChange := mon.IsMajorChangeFrom(delta.Old, delta.New) isMajorChange := mon.IsMajorChangeFrom(delta.Old, delta.New)
-26
View File
@@ -3,7 +3,6 @@ package cli
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"io" "io"
"net" "net"
"os/exec" "os/exec"
@@ -50,28 +49,3 @@ func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bo
_, ok := validIfacesMap[iface.Name] _, ok := validIfacesMap[iface.Name]
return ok return ok
} }
// validInterfacesMap returns a set of all valid hardware ports.
func validInterfacesMap(ctx context.Context) map[string]struct{} {
b, err := exec.Command("networksetup", "-listallhardwareports").Output()
if err != nil {
return nil
}
return parseListAllHardwarePorts(bytes.NewReader(b))
}
// parseListAllHardwarePorts parses output of "networksetup -listallhardwareports"
// and returns map presents all hardware ports.
func parseListAllHardwarePorts(r io.Reader) map[string]struct{} {
m := make(map[string]struct{})
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
after, ok := strings.CutPrefix(line, "Device: ")
if !ok {
continue
}
m[after] = struct{}{}
}
return m
}
-50
View File
@@ -1,15 +1,7 @@
package cli package cli
import ( import (
"context"
"net" "net"
"net/netip"
"os"
"strings"
"tailscale.com/net/netmon"
"github.com/Control-D-Inc/ctrld"
) )
// patchNetIfaceName patches network interface names on Linux // patchNetIfaceName patches network interface names on Linux
@@ -23,45 +15,3 @@ func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bo
_, ok := validIfacesMap[iface.Name] _, ok := validIfacesMap[iface.Name]
return ok return ok
} }
// validInterfacesMap returns a set containing non virtual interfaces.
// This filters out virtual interfaces to ensure DNS is only configured on physical interfaces
func validInterfacesMap(ctx context.Context) map[string]struct{} {
m := make(map[string]struct{})
vis := virtualInterfaces(ctx)
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
if _, existed := vis[i.Name]; existed {
return
}
m[i.Name] = struct{}{}
})
// Fallback to the default route interface if found nothing.
// This ensures we always have at least one interface to configure
if len(m) == 0 {
defaultRoute, err := netmon.DefaultRoute()
if err != nil {
return m
}
m[defaultRoute.InterfaceName] = struct{}{}
}
return m
}
// virtualInterfaces returns a map of virtual interfaces on the current machine.
// This reads from /sys/devices/virtual/net to identify virtual network interfaces
// Virtual interfaces should not have DNS configured as they don't represent physical network connections
func virtualInterfaces(ctx context.Context) map[string]struct{} {
logger := ctrld.LoggerFromCtx(ctx)
s := make(map[string]struct{})
entries, err := os.ReadDir("/sys/devices/virtual/net")
if err != nil {
logger.Error().Err(err).Msg("Failed to read /sys/devices/virtual/net")
return nil
}
for _, entry := range entries {
if entry.IsDir() {
s[strings.TrimSpace(entry.Name())] = struct{}{}
}
}
return s
}
-12
View File
@@ -3,10 +3,7 @@
package cli package cli
import ( import (
"context"
"net" "net"
"tailscale.com/net/netmon"
) )
// patchNetIfaceName patches network interface names on non-Linux/Darwin platforms // patchNetIfaceName patches network interface names on non-Linux/Darwin platforms
@@ -14,12 +11,3 @@ func patchNetIfaceName(iface *net.Interface) (bool, error) { return true, nil }
// validInterface checks if an interface is valid on non-Linux/Darwin platforms // validInterface checks if an interface is valid on non-Linux/Darwin platforms
func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool { return true } func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool { return true }
// validInterfacesMap returns a set containing only default route interfaces.
func validInterfacesMap(ctx context.Context) map[string]struct{} {
defaultRoute, err := netmon.DefaultRoute()
if err != nil {
return nil
}
return map[string]struct{}{defaultRoute.InterfaceName: {}}
}
-12
View File
@@ -1,10 +1,7 @@
package cli package cli
import ( import (
"context"
"net" "net"
"github.com/Control-D-Inc/ctrld"
) )
func patchNetIfaceName(iface *net.Interface) (bool, error) { func patchNetIfaceName(iface *net.Interface) (bool, error) {
@@ -17,12 +14,3 @@ func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bo
_, ok := validIfacesMap[iface.Name] _, ok := validIfacesMap[iface.Name]
return ok return ok
} }
// validInterfacesMap returns a set of all physical interfaces.
func validInterfacesMap(ctx context.Context) map[string]struct{} {
m := make(map[string]struct{})
for ifaceName := range ctrld.ValidInterfaces(ctx) {
m[ifaceName] = struct{}{}
}
return m
}
+1 -1
View File
@@ -1291,7 +1291,7 @@ func canBeLocalUpstream(addr string) bool {
// the interface that matches excludeIfaceName. The context is used to clarify the // the interface that matches excludeIfaceName. The context is used to clarify the
// log message when error happens. // log message when error happens.
func withEachPhysicalInterfaces(excludeIfaceName, contextStr string, f func(i *net.Interface) error) { func withEachPhysicalInterfaces(excludeIfaceName, contextStr string, f func(i *net.Interface) error) {
validIfacesMap := validInterfacesMap(ctrld.LoggerCtx(context.Background(), mainLog.Load())) validIfacesMap := ctrld.ValidInterfaces(ctrld.LoggerCtx(context.Background(), mainLog.Load()))
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
// Skip loopback/virtual/down interface. // Skip loopback/virtual/down interface.
if i.IsLoopback() || len(i.HardwareAddr) == 0 { if i.IsLoopback() || len(i.HardwareAddr) == 0 {
+21 -26
View File
@@ -24,7 +24,7 @@ func dnsFns() []dnsFn {
return []dnsFn{dnsFromResolvConf, dns4, dns6, dnsFromSystemdResolver} return []dnsFn{dnsFromResolvConf, dns4, dns6, dnsFromSystemdResolver}
} }
func dns4(_ context.Context) []string { func dns4(ctx context.Context) []string {
f, err := os.Open(v4RouteFile) f, err := os.Open(v4RouteFile)
if err != nil { if err != nil {
return nil return nil
@@ -33,7 +33,7 @@ func dns4(_ context.Context) []string {
var dns []string var dns []string
seen := make(map[string]bool) seen := make(map[string]bool)
vis := virtualInterfaces() vis := virtualInterfaces(ctx)
s := bufio.NewScanner(f) s := bufio.NewScanner(f)
first := true first := true
for s.Scan() { for s.Scan() {
@@ -46,7 +46,7 @@ func dns4(_ context.Context) []string {
continue continue
} }
// Skip virtual interfaces. // Skip virtual interfaces.
if vis.contains(string(bytes.TrimSpace(fields[0]))) { if _, ok := vis[string(bytes.TrimSpace(fields[0]))]; ok {
continue continue
} }
gw := make([]byte, net.IPv4len) gw := make([]byte, net.IPv4len)
@@ -64,7 +64,7 @@ func dns4(_ context.Context) []string {
return dns return dns
} }
func dns6(_ context.Context) []string { func dns6(ctx context.Context) []string {
f, err := os.Open(v6RouteFile) f, err := os.Open(v6RouteFile)
if err != nil { if err != nil {
return nil return nil
@@ -72,7 +72,7 @@ func dns6(_ context.Context) []string {
defer f.Close() defer f.Close()
var dns []string var dns []string
vis := virtualInterfaces() vis := virtualInterfaces(ctx)
s := bufio.NewScanner(f) s := bufio.NewScanner(f)
for s.Scan() { for s.Scan() {
fields := bytes.Fields(s.Bytes()) fields := bytes.Fields(s.Bytes())
@@ -80,7 +80,7 @@ func dns6(_ context.Context) []string {
continue continue
} }
// Skip virtual interfaces. // Skip virtual interfaces.
if vis.contains(string(bytes.TrimSpace(fields[len(fields)-1]))) { if _, ok := vis[string(bytes.TrimSpace(fields[len(fields)-1]))]; ok {
continue continue
} }
@@ -110,34 +110,29 @@ func dnsFromSystemdResolver(_ context.Context) []string {
return ns return ns
} }
type set map[string]struct{} // virtualInterfaces returns a map of virtual interfaces on the current machine.
// This reads from /sys/devices/virtual/net to identify virtual network interfaces
func (s *set) add(e string) { // Virtual interfaces should not have DNS configured as they don't represent physical network connections
(*s)[e] = struct{}{} func virtualInterfaces(ctx context.Context) map[string]struct{} {
} logger := LoggerFromCtx(ctx)
s := make(map[string]struct{})
func (s *set) contains(e string) bool { entries, err := os.ReadDir("/sys/devices/virtual/net")
_, ok := (*s)[e] if err != nil {
return ok logger.Error().Err(err).Msg("Failed to read /sys/devices/virtual/net")
} return nil
}
// virtualInterfaces returns a set of virtual interfaces on current machine.
func virtualInterfaces() set {
s := make(set)
entries, _ := os.ReadDir("/sys/devices/virtual/net")
for _, entry := range entries { for _, entry := range entries {
if entry.IsDir() { if entry.IsDir() {
s.add(strings.TrimSpace(entry.Name())) s[strings.TrimSpace(entry.Name())] = struct{}{}
} }
} }
return s return s
} }
// validInterfacesMap returns a set containing non virtual interfaces. // ValidInterfaces returns a set containing non virtual interfaces.
// TODO: deduplicated with cmd/cli/net_linux.go in v2. func ValidInterfaces(ctx context.Context) map[string]struct{} {
func validInterfaces() set {
m := make(map[string]struct{}) m := make(map[string]struct{})
vis := virtualInterfaces() vis := virtualInterfaces(ctx)
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
if _, existed := vis[i.Name]; existed { if _, existed := vis[i.Name]; existed {
return return
+2 -1
View File
@@ -1,10 +1,11 @@
package ctrld package ctrld
import ( import (
"context"
"testing" "testing"
) )
func Test_virtualInterfaces(t *testing.T) { func Test_virtualInterfaces(t *testing.T) {
vis := virtualInterfaces() vis := virtualInterfaces(context.Background())
t.Log(vis) t.Log(vis)
} }
-4
View File
@@ -444,7 +444,3 @@ func ValidInterfaces(ctx context.Context) map[string]struct{} {
} }
return m return m
} }
func validInterfaces() map[string]struct{} {
return ValidInterfaces(context.Background())
}
+3 -3
View File
@@ -3,14 +3,14 @@ package ctrld
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"context"
"io" "io"
"os/exec" "os/exec"
"strings" "strings"
) )
// validInterfaces returns a set of all valid hardware ports. // ValidInterfaces returns a set of all valid hardware ports.
// TODO: deduplicated with cmd/cli/net_darwin.go in v2. func ValidInterfaces(_ context.Context) map[string]struct{} {
func validInterfaces() map[string]struct{} {
b, err := exec.Command("networksetup", "-listallhardwareports").Output() b, err := exec.Command("networksetup", "-listallhardwareports").Output()
if err != nil { if err != nil {
return nil return nil
+7 -4
View File
@@ -2,11 +2,14 @@
package ctrld package ctrld
import "tailscale.com/net/netmon" import (
"context"
// validInterfaces returns a set containing only default route interfaces. "tailscale.com/net/netmon"
// TODO: deuplicated with cmd/cli/net_others.go in v2. )
func validInterfaces() map[string]struct{} {
// ValidInterfaces returns a set containing only default route interfaces.
func ValidInterfaces(_ context.Context) map[string]struct{} {
defaultRoute, err := netmon.DefaultRoute() defaultRoute, err := netmon.DefaultRoute()
if err != nil { if err != nil {
return nil return nil
+1 -1
View File
@@ -711,7 +711,7 @@ func newResolverWithNameserver(nameservers []string) *osResolver {
// Rfc1918Addresses returns the list of local physical interfaces private IP addresses // Rfc1918Addresses returns the list of local physical interfaces private IP addresses
func Rfc1918Addresses() []string { func Rfc1918Addresses() []string {
vis := validInterfaces() vis := ValidInterfaces(context.Background())
var res []string var res []string
netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) { netmon.ForeachInterface(func(i netmon.Interface, prefixes []netip.Prefix) {
// Skip virtual interfaces. // Skip virtual interfaces.