diff --git a/cmd/cli/net_darwin.go b/cmd/cli/net_darwin.go index f456327..37f8d7b 100644 --- a/cmd/cli/net_darwin.go +++ b/cmd/cli/net_darwin.go @@ -42,3 +42,21 @@ func networkServiceName(ifaceName string, r io.Reader) string { } return "" } + +// validInterface reports whether the *net.Interface is a valid one, which includes: +// +// - en0: physical wireless +// - en1: Thunderbolt 1 +// - en2: Thunderbolt 2 +// - en3: Thunderbolt 3 +// - en4: Thunderbolt 4 +// +// For full list, see: https://unix.stackexchange.com/questions/603506/what-are-these-ifconfig-interfaces-on-macos +func validInterface(iface *net.Interface) bool { + switch iface.Name { + case "en0", "en1", "en2", "en3", "en4": + return true + default: + return false + } +} diff --git a/cmd/cli/net_others.go b/cmd/cli/net_others.go index 2f7aec8..c27a608 100644 --- a/cmd/cli/net_others.go +++ b/cmd/cli/net_others.go @@ -5,3 +5,5 @@ package cli import "net" func patchNetIfaceName(iface *net.Interface) error { return nil } + +func validInterface(iface *net.Interface) bool { return true } diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 4b3968f..8d0cf3e 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -13,6 +13,7 @@ import ( "runtime" "sort" "strconv" + "strings" "sync" "syscall" @@ -417,8 +418,14 @@ func (p *prog) setDNS() { if iface == "" { return } + // allIfaces tracks whether we should set DNS for all physical interfaces. + allIfaces := false if iface == "auto" { iface = defaultIfaceName() + // If iface is "auto", it means user does not specify "--iface" flag. + // In this case, ctrld has to set DNS for all physical interfaces, so + // thing will still work when user switch from one to the other. + allIfaces = requiredMultiNICsConfig() } lc := cfg.FirstListener() if lc == nil { @@ -460,14 +467,22 @@ func (p *prog) setDNS() { return } logger.Debug().Msg("setting DNS successfully") + if allIfaces { + withEachPhysicalInterfaces(netIface.Name, "set DNS", func(i *net.Interface) error { + return setDNS(i, nameservers) + }) + } } func (p *prog) resetDNS() { if iface == "" { return } + allIfaces := false if iface == "auto" { iface = defaultIfaceName() + // See corresponding comments in (*prog).setDNS function. + allIfaces = requiredMultiNICsConfig() } logger := mainLog.Load().With().Str("iface", iface).Logger() netIface, err := netInterface(iface) @@ -485,6 +500,9 @@ func (p *prog) resetDNS() { return } logger.Debug().Msg("Restoring DNS successfully") + if allIfaces { + withEachPhysicalInterfaces(netIface.Name, "reset DNS", resetDNS) + } } func randomLocalIP() string { @@ -649,3 +667,51 @@ func canBeLocalUpstream(addr string) bool { } return false } + +// withEachPhysicalInterfaces runs the function f with each physical interfaces, excluding +// the interface that matches excludeIfaceName. The context is used to clarify the +// log message when error happens. +func withEachPhysicalInterfaces(excludeIfaceName, context string, f func(i *net.Interface) error) { + interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + // Skip not-running/local/virtual interface. + if !i.IsUp() || i.IsLoopback() || len(i.HardwareAddr) == 0 { + return + } + // Skip non-configured interfaces. + if addrs, _ := i.Addrs(); len(addrs) == 0 { + return + } + // Skip invalid interface. + if !validInterface(i.Interface) { + return + } + netIface := i.Interface + if err := patchNetIfaceName(netIface); err != nil { + mainLog.Load().Debug().Err(err).Msg("failed to patch net interface name") + return + } + // Skip excluded interface. + if netIface.Name == excludeIfaceName { + return + } + // Skip Windows Hyper-V Default Switch. + if strings.Contains(netIface.Name, "vEthernet") { + return + } + if err := f(netIface); err != nil { + mainLog.Load().Warn().Err(err).Msgf("failed to %s for interface: %q", context, i.Name) + } else { + mainLog.Load().Debug().Msgf("%s for interface %q successfully", context, i.Name) + } + }) +} + +// requiredMultiNicConfig reports whether ctrld needs to set/reset DNS for multiple NICs. +func requiredMultiNICsConfig() bool { + switch runtime.GOOS { + case "windows", "darwin": + return true + default: + return false + } +}