query for connectivity

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
aaron/oss_17111
Aaron Klotz 3 months ago
parent b7658a4ad2
commit 7a76b22972

@ -4,7 +4,9 @@
package netmon package netmon
import ( import (
"cmp"
"log" "log"
"net"
"net/netip" "net/netip"
"net/url" "net/url"
"strings" "strings"
@ -15,6 +17,7 @@ import (
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/feature/buildfeatures" "tailscale.com/feature/buildfeatures"
"tailscale.com/tsconst" "tailscale.com/tsconst"
"tailscale.com/util/winutil/winnet"
) )
const ( const (
@ -22,12 +25,118 @@ const (
) )
func init() { func init() {
altNetInterfaces = altNetInterfacesWindows
likelyHomeRouterIP = likelyHomeRouterIPWindows likelyHomeRouterIP = likelyHomeRouterIPWindows
if buildfeatures.HasUseProxy { if buildfeatures.HasUseProxy {
getPAC = getPACWindows 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) { func likelyHomeRouterIPWindows() (ret netip.Addr, _ netip.Addr, ok bool) {
rs, err := winipcfg.GetIPForwardTable2(windows.AF_INET) rs, err := winipcfg.GetIPForwardTable2(windows.AF_INET)
if err != nil { if err != nil {

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

@ -142,15 +142,29 @@ func sortIPs(s []netip.Addr) {
sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) }) 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. // Interface is a wrapper around Go's net.Interface with some extra methods.
type Interface struct { type Interface struct {
*net.Interface *net.Interface
AltAddrs []net.Addr // if non-nil, returned by Addrs AltAddrs []net.Addr // if non-nil, returned by Addrs
Desc string // extra description (used on Windows) 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) { func (i Interface) Addrs() ([]net.Addr, error) {
if i.AltAddrs != nil { if i.AltAddrs != nil {
return i.AltAddrs, nil return i.AltAddrs, nil
@ -158,6 +172,14 @@ func (i Interface) Addrs() ([]net.Addr, error) {
return i.Interface.Addrs() 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 // ForeachInterfaceAddress is a wrapper for GetList, then
// List.ForeachInterfaceAddress. // List.ForeachInterfaceAddress.
func ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error { func ForeachInterfaceAddress(fn func(Interface, netip.Prefix)) error {
@ -492,8 +514,8 @@ func getState(optTSInterfaceName string) (*State, error) {
if pfx.Addr().IsLoopback() { if pfx.Addr().IsLoopback() {
continue continue
} }
s.HaveV6 = s.HaveV6 || isUsableV6(pfx.Addr()) s.HaveV6 = s.HaveV6 || (isUsableV6(pfx.Addr()) && !ni.isNoIPv6InternetConnectivity())
s.HaveV4 = s.HaveV4 || isUsableV4(pfx.Addr()) s.HaveV4 = s.HaveV4 || (isUsableV4(pfx.Addr()) && !ni.isNoIPv4InternetConnectivity())
} }
}); err != nil { }); err != nil {
return nil, err return nil, err

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

@ -1,27 +1,67 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build windows
// Package winnet contains Windows-specific networking code. // Package winnet contains Windows-specific networking code.
package winnet package winnet
import ( import (
"fmt" "fmt"
"sync"
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/go-ole/go-ole" "github.com/go-ole/go-ole"
"github.com/go-ole/go-ole/oleutil" "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_INetwork = ole.NewGUID("{8A40A45D-055C-4B62-ABD7-6D613E2CEAEC}")
var IID_INetworkConnection = ole.NewGUID("{DCB00005-570F-4A9B-8D69-199FDBA5723B}") var IID_INetworkConnection = ole.NewGUID("{DCB00005-570F-4A9B-8D69-199FDBA5723B}")
type NetworkListManager struct { 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 { type INetworkConnection struct {
@ -62,25 +102,29 @@ type INetworkVtbl struct {
SetCategory uintptr SetCategory uintptr
} }
func NewNetworkListManager(c *ole.Connection) (*NetworkListManager, error) { func newNetworkListManager() (*NetworkListManager, error) {
err := c.Create(CLSID_NetworkListManager) unk, err := ole.CreateInstance(CLSID_NetworkListManager, IID_INetworkListManager)
if err != nil {
return nil, err
}
defer c.Release()
d, err := c.Dispatch()
if err != nil { if err != nil {
return nil, err return nil, err
} }
nlm := (*INetworkListManager)(unsafe.Pointer(unk))
return &NetworkListManager{ return &NetworkListManager{
d: d, i: nlm,
}, nil }, nil
} }
func (m *NetworkListManager) Release() { var (
m.d.Release() once sync.Once
nlm *NetworkListManager
nlmErr error
)
func GetNetworkListManager() (*NetworkListManager, error) {
once.Do(func() {
nlm, nlmErr = newNetworkListManager()
})
return nlm, nlmErr
} }
func (cl ConnectionList) Release() { 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) { 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 { if err != nil {
return nil, err return nil, err
} }
@ -168,6 +215,20 @@ func (n *INetwork) SetCategory(v int32) error {
return nil 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 { func (n *INetwork) VTable() *INetworkVtbl {
return (*INetworkVtbl)(unsafe.Pointer(n.RawVTable)) return (*INetworkVtbl)(unsafe.Pointer(n.RawVTable))
} }
@ -190,3 +251,17 @@ func (v *INetworkConnection) GetNetwork() (*INetwork, error) {
return result, nil 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
}

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

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

@ -18,10 +18,9 @@ import (
"tailscale.com/net/netmon" "tailscale.com/net/netmon"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/net/tstun" "tailscale.com/net/tstun"
"tailscale.com/util/winutil/winnet"
"tailscale.com/wgengine/router" "tailscale.com/wgengine/router"
"tailscale.com/wgengine/winnet"
ole "github.com/go-ole/go-ole"
"github.com/tailscale/wireguard-go/tun" "github.com/tailscale/wireguard-go/tun"
"go4.org/netipx" "go4.org/netipx"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
@ -175,15 +174,10 @@ func setPrivateNetwork(ifcLUID winipcfg.LUID) (bool, error) {
return false, fmt.Errorf("ifcLUID.GUID: %v", err) return false, fmt.Errorf("ifcLUID.GUID: %v", err)
} }
// aaron: DO NOT call Initialize() or Uninitialize() on c! m, err := winnet.GetNetworkListManager()
// We've already handled that process-wide.
var c ole.Connection
m, err := winnet.NewNetworkListManager(&c)
if err != nil { if err != nil {
return false, fmt.Errorf("winnet.NewNetworkListManager: %v", err) return false, fmt.Errorf("winnet.NewNetworkListManager: %v", err)
} }
defer m.Release()
cl, err := m.GetNetworkConnections() cl, err := m.GetNetworkConnections()
if err != nil { if err != nil {

@ -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
}
Loading…
Cancel
Save