mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
The journal records the network state of the host and each change of that state. The support team reads one file to see the interfaces, the routes, the resolvers, and the recoveries around an incident. The same file shows the health of the query path. A change that touches only AirDrop or virtual interfaces runs no DNS work.
245 lines
7.5 KiB
Go
245 lines
7.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/microsoft/wmi/pkg/base/host"
|
|
"github.com/microsoft/wmi/pkg/base/instance"
|
|
"github.com/microsoft/wmi/pkg/base/query"
|
|
"github.com/microsoft/wmi/pkg/constant"
|
|
"github.com/microsoft/wmi/pkg/hardware/network/netadapter"
|
|
)
|
|
|
|
// adapterInfo holds what one WMI query knows about a network adapter.
|
|
type adapterInfo struct {
|
|
Name string
|
|
Description string
|
|
Virtual bool
|
|
ConnectorPresent bool
|
|
Hardware bool
|
|
}
|
|
|
|
// Every network change reads the adapter set, so one query serves a window.
|
|
const adapterCacheTTL = time.Minute
|
|
|
|
var (
|
|
adapterCacheMu sync.Mutex
|
|
adapterCache []adapterInfo
|
|
// The set before the last read still names the adapter that went away,
|
|
// so a removed adapter keeps its class in the journal.
|
|
adapterCachePrevious []adapterInfo
|
|
adapterCacheReadAt time.Time
|
|
|
|
// Tests replace these seams to avoid a WMI query.
|
|
readAdaptersFn = readAdapters
|
|
adapterCacheNowFn = time.Now
|
|
)
|
|
|
|
func patchNetIfaceName(iface *net.Interface) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
// validInterface reports whether the *net.Interface is a valid one.
|
|
// On Windows, only physical interfaces are considered valid.
|
|
func validInterface(iface *net.Interface, validIfacesMap map[string]struct{}) bool {
|
|
_, ok := validIfacesMap[iface.Name]
|
|
return ok
|
|
}
|
|
|
|
// validInterfacesMap returns a set of all physical interfaces.
|
|
func validInterfacesMap() map[string]struct{} {
|
|
m := make(map[string]struct{})
|
|
for _, ifaceName := range validInterfaces() {
|
|
m[ifaceName] = struct{}{}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// validInterfaces returns a list of all physical interfaces.
|
|
func validInterfaces() []string {
|
|
var names []string
|
|
for _, adapter := range adapters() {
|
|
if !adapter.ConnectorPresent {
|
|
mainLog.Load().Debug().Str("method", "validInterfaces").Str("interface", adapter.Name).Msg("skipping non-physical adapter")
|
|
continue
|
|
}
|
|
if !adapter.Hardware {
|
|
mainLog.Load().Debug().Str("method", "validInterfaces").Str("interface", adapter.Name).Msg("skipping non-hardware interface")
|
|
continue
|
|
}
|
|
names = append(names, adapter.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// adapters returns the adapter set of the host, from the cache while it is
|
|
// younger than the window. The query runs under the mutex, so two callers
|
|
// start one query.
|
|
func adapters() []adapterInfo {
|
|
adapterCacheMu.Lock()
|
|
defer adapterCacheMu.Unlock()
|
|
now := adapterCacheNowFn()
|
|
if !adapterCacheReadAt.IsZero() && now.Sub(adapterCacheReadAt) < adapterCacheTTL {
|
|
return adapterCache
|
|
}
|
|
read := readAdaptersFn()
|
|
// A failing query waits for the window too, so it starts no query storm.
|
|
adapterCacheReadAt = now
|
|
if read == nil {
|
|
// Keep the last good read, so a failed query loses no adapter.
|
|
return adapterCache
|
|
}
|
|
adapterCachePrevious, adapterCache = adapterCache, read
|
|
return read
|
|
}
|
|
|
|
// refreshInterfaceMeta drops the read window. The cache still describes the
|
|
// adapter that appeared or went away, and the next lookup reads the set.
|
|
func refreshInterfaceMeta() {
|
|
adapterCacheMu.Lock()
|
|
defer adapterCacheMu.Unlock()
|
|
adapterCacheReadAt = time.Time{}
|
|
}
|
|
|
|
// tunnelAdapterWords name the VPN drivers of Windows. The description is the
|
|
// only signal, because WMI describes a tunnel adapter as an Ethernet adapter.
|
|
var tunnelAdapterWords = []string{"tap", "wintun", "wireguard", "vpn"}
|
|
|
|
// adapterClass names the kind of an adapter. WMI marks a software adapter
|
|
// virtual and gives it no connector.
|
|
func adapterClass(adapter adapterInfo) string {
|
|
switch {
|
|
case tunnelAdapter(adapter.Description):
|
|
return "tunnel"
|
|
case adapter.Virtual && !adapter.ConnectorPresent:
|
|
return "virtual"
|
|
}
|
|
return "hardware"
|
|
}
|
|
|
|
// tunnelAdapter reports a description that names a VPN driver.
|
|
func tunnelAdapter(description string) bool {
|
|
lower := strings.ToLower(description)
|
|
for _, word := range tunnelAdapterWords {
|
|
if strings.Contains(lower, word) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// platformInterfaceMeta returns the Windows class and adapter description of
|
|
// an interface. Windows has no service name.
|
|
func platformInterfaceMeta(name string) (class, hardwarePort, service string) {
|
|
adapter, known := adapterByName(name)
|
|
if !known {
|
|
return "", "", ""
|
|
}
|
|
return adapterClass(adapter), adapter.Description, ""
|
|
}
|
|
|
|
// adapterByName finds an adapter in the current set, or in the set before the
|
|
// last read. An adapter that went away keeps its names that way.
|
|
func adapterByName(name string) (adapterInfo, bool) {
|
|
if adapter, known := findAdapter(adapters(), name); known {
|
|
return adapter, true
|
|
}
|
|
adapterCacheMu.Lock()
|
|
defer adapterCacheMu.Unlock()
|
|
return findAdapter(adapterCachePrevious, name)
|
|
}
|
|
|
|
func findAdapter(set []adapterInfo, name string) (adapterInfo, bool) {
|
|
for _, adapter := range set {
|
|
if adapter.Name == name {
|
|
return adapter, true
|
|
}
|
|
}
|
|
return adapterInfo{}, false
|
|
}
|
|
|
|
// platformVirtualInterfaces returns the adapters that carry no traffic of the
|
|
// host network.
|
|
func platformVirtualInterfaces() map[string]struct{} {
|
|
virtual := make(map[string]struct{})
|
|
for _, adapter := range adapters() {
|
|
if adapterClass(adapter) != "virtual" {
|
|
continue
|
|
}
|
|
virtual[adapter.Name] = struct{}{}
|
|
}
|
|
return virtual
|
|
}
|
|
|
|
// readAdapters runs one WMI query for every network adapter. It returns nil
|
|
// when the query fails.
|
|
func readAdapters() []adapterInfo {
|
|
log.SetOutput(io.Discard)
|
|
defer log.SetOutput(os.Stderr)
|
|
whost := host.NewWmiLocalHost()
|
|
q := query.NewWmiQuery("MSFT_NetAdapter")
|
|
instances, err := instance.GetWmiInstancesFromHost(whost, string(constant.StadardCimV2), q)
|
|
if instances != nil {
|
|
defer instances.Close()
|
|
}
|
|
if err != nil {
|
|
mainLog.Load().Warn().Err(err).Msg("failed to get wmi network adapter")
|
|
return nil
|
|
}
|
|
var read []adapterInfo
|
|
for _, i := range instances {
|
|
adapter, err := netadapter.NewNetworkAdapter(i)
|
|
if err != nil {
|
|
mainLog.Load().Warn().Err(err).Msg("failed to get network adapter")
|
|
continue
|
|
}
|
|
info, ok := readAdapterInfo(adapter)
|
|
if !ok {
|
|
continue
|
|
}
|
|
read = append(read, info)
|
|
}
|
|
return read
|
|
}
|
|
|
|
// readAdapterInfo reads the properties of one adapter. It reports false when a
|
|
// property that decides validity is unreadable.
|
|
func readAdapterInfo(adapter *netadapter.NetworkAdapter) (adapterInfo, bool) {
|
|
name, err := adapter.GetPropertyName()
|
|
if err != nil {
|
|
mainLog.Load().Warn().Err(err).Msg("failed to get interface name")
|
|
return adapterInfo{}, false
|
|
}
|
|
|
|
// From: https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/hh968170(v=vs.85)
|
|
//
|
|
// "Indicates if a connector is present on the network adapter. This value is set to TRUE
|
|
// if this is a physical adapter or FALSE if this is not a physical adapter."
|
|
connectorPresent, err := adapter.GetPropertyConnectorPresent()
|
|
if err != nil {
|
|
mainLog.Load().Debug().Str("method", "readAdapterInfo").Str("interface", name).Msg("failed to get network adapter connector present property")
|
|
return adapterInfo{}, false
|
|
}
|
|
|
|
// Check if it's a hardware interface. Checking only for connector present is not enough
|
|
// because some interfaces are not physical but have a connector.
|
|
hardware, err := adapter.GetPropertyHardwareInterface()
|
|
if err != nil {
|
|
mainLog.Load().Debug().Str("method", "readAdapterInfo").Str("interface", name).Msg("failed to get network adapter hardware interface property")
|
|
return adapterInfo{}, false
|
|
}
|
|
|
|
info := adapterInfo{Name: name, ConnectorPresent: connectorPresent, Hardware: hardware}
|
|
// The class and the description describe the adapter only, so an
|
|
// unreadable one costs no validity.
|
|
info.Description, _ = adapter.GetPropertyInterfaceDescription()
|
|
info.Virtual, _ = adapter.GetPropertyVirtual()
|
|
return info, true
|
|
}
|