diff --git a/net/netmon/interfaces_windows.go b/net/netmon/interfaces_windows.go index d6625ead3..d6403e8ad 100644 --- a/net/netmon/interfaces_windows.go +++ b/net/netmon/interfaces_windows.go @@ -4,7 +4,9 @@ package netmon import ( + "cmp" "log" + "net" "net/netip" "net/url" "strings" @@ -15,6 +17,7 @@ import ( "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/feature/buildfeatures" "tailscale.com/tsconst" + "tailscale.com/util/winutil/winnet" ) const ( @@ -22,12 +25,118 @@ const ( ) func init() { + altNetInterfaces = altNetInterfacesWindows likelyHomeRouterIP = likelyHomeRouterIPWindows if buildfeatures.HasUseProxy { getPAC = getPACWindows } } +func altNetInterfacesWindows() ([]Interface, error) { + adapterAddrs, err := getInterfaces(windows.AF_UNSPEC, winipcfg.GAAFlagIncludePrefix|winipcfg.GAAFlagIncludeGateways, notTailscaleInterface) + if err != nil { + return nil, err + } + + result := make([]Interface, 0, len(adapterAddrs)) + + for _, aa := range adapterAddrs { + curIface := net.Interface{ + Index: int(cmp.Or(aa.IfIndex, aa.IPv6IfIndex)), + Name: aa.FriendlyName(), + } + + if aa.OperStatus == windows.IfOperStatusUp { + curIface.Flags |= net.FlagUp + curIface.Flags |= net.FlagRunning + } + + platFlags := connectivityFlags(aa.NetworkGUID) + + entry, err := ifEntry(aa.LUID) + if err != nil { + return nil, err + } + + eat := entry.AccessType + if eat&winipcfg.NetIfAccessLoopback != 0 { + curIface.Flags |= net.FlagLoopback + } + if eat&winipcfg.NetIfAccessBroadcast != 0 { + curIface.Flags |= net.FlagBroadcast + } + if eat&winipcfg.NetIfAccessPointToPoint != 0 { + curIface.Flags |= net.FlagPointToPoint + } + if eat&winipcfg.NetIfAccessPointToMultiPoint != 0 { + curIface.Flags |= net.FlagMulticast + } + + if aa.MTU == 0xffffffff { + curIface.MTU = -1 + } else { + curIface.MTU = int(aa.MTU) + } + + if physAddr := aa.PhysicalAddress(); len(physAddr) > 0 { + curIface.HardwareAddr = make([]byte, len(physAddr)) + copy(curIface.HardwareAddr, physAddr) + } + + result = append(result, Interface{ + Interface: &curIface, + Desc: aa.Description(), + PlatFlags: platFlags, + }) + } + + return result, nil +} + +func connectivityFlags(ifGUID windows.GUID) (flags PlatFlags) { + nlm, err := winnet.GetNetworkListManager() + if err != nil { + return 0 + } + + network, err := nlm.GetNetwork(ifGUID) + if err != nil { + return 0 + } + defer network.Release() + + connectivity, err := network.GetConnectivity() + if err != nil { + return 0 + } + + if connectivity&winnet.NLM_CONNECTIVITY_IPV4_INTERNET == 0 { + flags |= PlatFlagNoIPv4InternetConnectivity + } + + if connectivity&winnet.NLM_CONNECTIVITY_IPV6_INTERNET == 0 { + flags |= PlatFlagNoIPv6InternetConnectivity + } + + return flags +} + +func ifEntry(ifLUID winipcfg.LUID) (*winipcfg.MibIfRow2, error) { + row := &winipcfg.MibIfRow2{ + InterfaceLUID: ifLUID, + } + if procGetIfEntry2Ex.Find() == nil { + if err := getIfEntry2Ex(_MibIfEntryNormalWithoutStatistics, row); err != nil { + return nil, err + } + } else { + if err := getIfEntry2(row); err != nil { + return nil, err + } + } + return row, nil +} + func likelyHomeRouterIPWindows() (ret netip.Addr, _ netip.Addr, ok bool) { rs, err := winipcfg.GetIPForwardTable2(windows.AF_INET) if err != nil { diff --git a/net/netmon/mksyscall.go b/net/netmon/mksyscall.go new file mode 100644 index 000000000..89f7820b1 --- /dev/null +++ b/net/netmon/mksyscall.go @@ -0,0 +1,17 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package netmon + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go +//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go + +type _MIB_IF_ENTRY_LEVEL int32 + +const ( + _MibIfEntryNormal _MIB_IF_ENTRY_LEVEL = 0 + _MibIfEntryNormalWithoutStatistics _MIB_IF_ENTRY_LEVEL = 2 +) + +//sys getIfEntry2(row *winipcfg.MibIfRow2) (ret error) = iphlpapi.GetIfEntry2 +//sys getIfEntry2Ex(level _MIB_IF_ENTRY_LEVEL, row *winipcfg.MibIfRow2) (ret error) = iphlpapi.GetIfEntry2Ex diff --git a/net/netmon/state.go b/net/netmon/state.go index 27e3524e8..bd8daf4fd 100644 --- a/net/netmon/state.go +++ b/net/netmon/state.go @@ -142,15 +142,29 @@ func sortIPs(s []netip.Addr) { sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) }) } +type PlatFlags uint + +const ( + PlatFlagNoIPv4InternetConnectivity PlatFlags = 1 << iota + PlatFlagNoIPv6InternetConnectivity +) + // Interface is a wrapper around Go's net.Interface with some extra methods. type Interface struct { *net.Interface - AltAddrs []net.Addr // if non-nil, returned by Addrs - Desc string // extra description (used on Windows) + AltAddrs []net.Addr // if non-nil, returned by Addrs + Desc string // extra description (used on Windows) + PlatFlags PlatFlags // flags with additional connectivity information +} + +func (i Interface) IsLoopback() bool { + return isLoopback(i.Interface) +} + +func (i Interface) IsUp() bool { + return isUp(i.Interface) } -func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) } -func (i Interface) IsUp() bool { return isUp(i.Interface) } func (i Interface) Addrs() ([]net.Addr, error) { if i.AltAddrs != nil { return i.AltAddrs, nil @@ -158,6 +172,14 @@ func (i Interface) Addrs() ([]net.Addr, error) { return i.Interface.Addrs() } +func (i Interface) isNoIPv6InternetConnectivity() bool { + return i.PlatFlags&PlatFlagNoIPv6InternetConnectivity != 0 +} + +func (i Interface) isNoIPv4InternetConnectivity() bool { + return i.PlatFlags&PlatFlagNoIPv6InternetConnectivity != 0 +} + // ForeachInterfaceAddress is a wrapper for GetList, then // List.ForeachInterfaceAddress. func ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error { @@ -492,8 +514,8 @@ func getState(optTSInterfaceName string) (*State, error) { if pfx.Addr().IsLoopback() { continue } - s.HaveV6 = s.HaveV6 || isUsableV6(pfx.Addr()) - s.HaveV4 = s.HaveV4 || isUsableV4(pfx.Addr()) + s.HaveV6 = s.HaveV6 || (isUsableV6(pfx.Addr()) && !ni.isNoIPv6InternetConnectivity()) + s.HaveV4 = s.HaveV4 || (isUsableV4(pfx.Addr()) && !ni.isNoIPv4InternetConnectivity()) } }); err != nil { return nil, err diff --git a/net/netmon/zsyscall_windows.go b/net/netmon/zsyscall_windows.go new file mode 100644 index 000000000..68d3230a9 --- /dev/null +++ b/net/netmon/zsyscall_windows.go @@ -0,0 +1,62 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package netmon + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + + procGetIfEntry2 = modiphlpapi.NewProc("GetIfEntry2") + procGetIfEntry2Ex = modiphlpapi.NewProc("GetIfEntry2Ex") +) + +func getIfEntry2(row *winipcfg.MibIfRow2) (ret error) { + r0, _, _ := syscall.Syscall(procGetIfEntry2.Addr(), 1, uintptr(unsafe.Pointer(row)), 0, 0) + if r0 != 0 { + ret = syscall.Errno(r0) + } + return +} + +func getIfEntry2Ex(level _MIB_IF_ENTRY_LEVEL, row *winipcfg.MibIfRow2) (ret error) { + r0, _, _ := syscall.Syscall(procGetIfEntry2Ex.Addr(), 2, uintptr(level), uintptr(unsafe.Pointer(row)), 0) + if r0 != 0 { + ret = syscall.Errno(r0) + } + return +} diff --git a/wgengine/winnet/winnet.go b/util/winutil/winnet/winnet_windows.go similarity index 58% rename from wgengine/winnet/winnet.go rename to util/winutil/winnet/winnet_windows.go index e04e6f5c5..f59cc7a9b 100644 --- a/wgengine/winnet/winnet.go +++ b/util/winutil/winnet/winnet_windows.go @@ -1,27 +1,67 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build windows - // Package winnet contains Windows-specific networking code. package winnet import ( "fmt" + "sync" "syscall" "unsafe" "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" + "golang.org/x/sys/windows" +) + +type NLM_CONNECTIVITY int32 + +const ( + NLM_CONNECTIVITY_DISCONNECTED NLM_CONNECTIVITY = 0 + NLM_CONNECTIVITY_IPV4_NOTRAFFIC NLM_CONNECTIVITY = 0x1 + NLM_CONNECTIVITY_IPV6_NOTRAFFIC NLM_CONNECTIVITY = 0x2 + NLM_CONNECTIVITY_IPV4_SUBNET NLM_CONNECTIVITY = 0x10 + NLM_CONNECTIVITY_IPV4_LOCALNETWORK NLM_CONNECTIVITY = 0x20 + NLM_CONNECTIVITY_IPV4_INTERNET NLM_CONNECTIVITY = 0x40 + NLM_CONNECTIVITY_IPV6_SUBNET NLM_CONNECTIVITY = 0x100 + NLM_CONNECTIVITY_IPV6_LOCALNETWORK NLM_CONNECTIVITY = 0x200 + NLM_CONNECTIVITY_IPV6_INTERNET NLM_CONNECTIVITY = 0x400 ) -const CLSID_NetworkListManager = "{DCB00C01-570F-4A9B-8D69-199FDBA5723B}" +var CLSID_NetworkListManager = ole.NewGUID("{DCB00C01-570F-4A9B-8D69-199FDBA5723B}") +var IID_INetworkListManager = ole.NewGUID("{DCB00000-570F-4A9B-8D69-199FDBA5723B}") var IID_INetwork = ole.NewGUID("{8A40A45D-055C-4B62-ABD7-6D613E2CEAEC}") var IID_INetworkConnection = ole.NewGUID("{DCB00005-570F-4A9B-8D69-199FDBA5723B}") type NetworkListManager struct { - d *ole.Dispatch + i *INetworkListManager +} + +func (m *NetworkListManager) GetNetwork(networkID windows.GUID) (*INetwork, error) { + return m.i.GetNetwork(networkID) +} + +type INetworkListManager struct { + ole.IUnknown +} + +func (i *INetworkListManager) VTable() *INetworkListManagerVtbl { + return (*INetworkListManagerVtbl)(unsafe.Pointer(i.RawVTable)) +} + +type INetworkListManagerVtbl struct { + ole.IDispatchVtbl + GetNetworks uintptr + GetNetwork uintptr + GetNetworkConnections uintptr + GetNetworkConnection uintptr + Get_IsConnectedToInternet uintptr + Get_IsConnected uintptr + GetConnectivity uintptr + SetSimulatedProfileInfo uintptr + ClearSimulatedProfileInfo uintptr } type INetworkConnection struct { @@ -62,25 +102,29 @@ type INetworkVtbl struct { SetCategory uintptr } -func NewNetworkListManager(c *ole.Connection) (*NetworkListManager, error) { - err := c.Create(CLSID_NetworkListManager) - if err != nil { - return nil, err - } - defer c.Release() - - d, err := c.Dispatch() +func newNetworkListManager() (*NetworkListManager, error) { + unk, err := ole.CreateInstance(CLSID_NetworkListManager, IID_INetworkListManager) if err != nil { return nil, err } + nlm := (*INetworkListManager)(unsafe.Pointer(unk)) return &NetworkListManager{ - d: d, + i: nlm, }, nil } -func (m *NetworkListManager) Release() { - m.d.Release() +var ( + once sync.Once + nlm *NetworkListManager + nlmErr error +) + +func GetNetworkListManager() (*NetworkListManager, error) { + once.Do(func() { + nlm, nlmErr = newNetworkListManager() + }) + return nlm, nlmErr } func (cl ConnectionList) Release() { @@ -103,7 +147,10 @@ func asIID(u ole.UnknownLike, iid *ole.GUID) (*ole.IDispatch, error) { } func (m *NetworkListManager) GetNetworkConnections() (ConnectionList, error) { - ncraw, err := m.d.Call("GetNetworkConnections") + d := ole.Dispatch{ + Object: (*ole.IDispatch)(unsafe.Pointer(m.i)), + } + ncraw, err := d.Call("GetNetworkConnections") if err != nil { return nil, err } @@ -168,6 +215,20 @@ func (n *INetwork) SetCategory(v int32) error { return nil } +func (n *INetwork) GetConnectivity() (c NLM_CONNECTIVITY, _ error) { + r, _, _ := syscall.SyscallN( + n.VTable().GetConnectivity, + uintptr(unsafe.Pointer(n)), + uintptr(unsafe.Pointer(&c)), + ) + + if int32(r) < 0 { + return 0, ole.NewError(r) + } + + return c, nil +} + func (n *INetwork) VTable() *INetworkVtbl { return (*INetworkVtbl)(unsafe.Pointer(n.RawVTable)) } @@ -190,3 +251,17 @@ func (v *INetworkConnection) GetNetwork() (*INetwork, error) { return result, nil } + +func (v *INetworkConnection) GetAdapterId() (string, error) { + buf := ole.GUID{} + hr, _, _ := syscall.Syscall( + v.VTable().GetAdapterId, + 2, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(&buf)), + 0) + if hr != 0 { + return "", fmt.Errorf("GetAdapterId failed: %08x", hr) + } + return buf.String(), nil +} diff --git a/util/winutil/winnet/winnet_windows_386.go b/util/winutil/winnet/winnet_windows_386.go new file mode 100644 index 000000000..88e5583f2 --- /dev/null +++ b/util/winutil/winnet/winnet_windows_386.go @@ -0,0 +1,32 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package winnet + +import ( + "syscall" + "unsafe" + + ole "github.com/go-ole/go-ole" + "golang.org/x/sys/windows" +) + +func (i *INetworkListManager) GetNetwork(networkID windows.GUID) (*INetwork, error) { + words := (*[4]uintptr)(unsafe.Pointer(&networkID)) + var result *INetwork + r, _, _ := syscall.SyscallN( + i.VTable().GetNetwork, + uintptr(unsafe.Pointer(i)), + words[0], + words[1], + words[2], + words[3], + uintptr(unsafe.Pointer(&result)), + ) + + if int32(r) < 0 { + return nil, ole.NewError(r) + } + + return result, nil +} diff --git a/util/winutil/winnet/winnet_windows_not386.go b/util/winutil/winnet/winnet_windows_not386.go new file mode 100644 index 000000000..762eb72d9 --- /dev/null +++ b/util/winutil/winnet/winnet_windows_not386.go @@ -0,0 +1,30 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build windows && !386 + +package winnet + +import ( + "syscall" + "unsafe" + + ole "github.com/go-ole/go-ole" + "golang.org/x/sys/windows" +) + +func (i *INetworkListManager) GetNetwork(networkID windows.GUID) (*INetwork, error) { + var result *INetwork + r, _, _ := syscall.SyscallN( + i.VTable().GetNetwork, + uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(&networkID)), + uintptr(unsafe.Pointer(&result)), + ) + + if int32(r) < 0 { + return nil, ole.NewError(r) + } + + return result, nil +} diff --git a/wgengine/router/osrouter/ifconfig_windows.go b/wgengine/router/osrouter/ifconfig_windows.go index cb87ad5f2..4c7586c52 100644 --- a/wgengine/router/osrouter/ifconfig_windows.go +++ b/wgengine/router/osrouter/ifconfig_windows.go @@ -18,10 +18,9 @@ import ( "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" "tailscale.com/net/tstun" + "tailscale.com/util/winutil/winnet" "tailscale.com/wgengine/router" - "tailscale.com/wgengine/winnet" - ole "github.com/go-ole/go-ole" "github.com/tailscale/wireguard-go/tun" "go4.org/netipx" "golang.org/x/sys/windows" @@ -175,15 +174,10 @@ func setPrivateNetwork(ifcLUID winipcfg.LUID) (bool, error) { return false, fmt.Errorf("ifcLUID.GUID: %v", err) } - // aaron: DO NOT call Initialize() or Uninitialize() on c! - // We've already handled that process-wide. - var c ole.Connection - - m, err := winnet.NewNetworkListManager(&c) + m, err := winnet.GetNetworkListManager() if err != nil { return false, fmt.Errorf("winnet.NewNetworkListManager: %v", err) } - defer m.Release() cl, err := m.GetNetworkConnections() if err != nil { diff --git a/wgengine/winnet/winnet_windows.go b/wgengine/winnet/winnet_windows.go deleted file mode 100644 index 283ce5ad1..000000000 --- a/wgengine/winnet/winnet_windows.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Tailscale Inc & AUTHORS -// SPDX-License-Identifier: BSD-3-Clause - -package winnet - -import ( - "fmt" - "syscall" - "unsafe" - - "github.com/go-ole/go-ole" -) - -func (v *INetworkConnection) GetAdapterId() (string, error) { - buf := ole.GUID{} - hr, _, _ := syscall.Syscall( - v.VTable().GetAdapterId, - 2, - uintptr(unsafe.Pointer(v)), - uintptr(unsafe.Pointer(&buf)), - 0) - if hr != 0 { - return "", fmt.Errorf("GetAdapterId failed: %08x", hr) - } - return buf.String(), nil -}