mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
c17a817769
The Layered Service Provider (LSP) is a deprecated (but still supported) mechanism for inserting user-mode DLLs into a filter chain between the Winsock API surface (ie, ws2_32.dll) and the internal user-mode interface to the networking stack. While their use is becoming more rare due to the aforementioned deprecation, it is still possible for third-party software to install their DLLs into this filter chain and interfere with Winsock API calls. Knowing whether this is happening is useful for troubleshooting. Fixes https://github.com/tailscale/tailscale/issues/8142 Signed-off-by: Aaron Klotz <aaron@tailscale.com>
485 lines
12 KiB
Go
485 lines
12 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package osdiag
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"unicode/utf16"
|
|
"unsafe"
|
|
|
|
"github.com/dblohm7/wingoes/pe"
|
|
"golang.org/x/sys/windows"
|
|
"golang.org/x/sys/windows/registry"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/winutil"
|
|
"tailscale.com/util/winutil/authenticode"
|
|
)
|
|
|
|
var (
|
|
errUnexpectedResult = errors.New("API call returned an unexpected value")
|
|
)
|
|
|
|
const (
|
|
maxBinaryValueLen = 128 // we'll truncate any binary values longer than this
|
|
maxRegValueNameLen = 16384 // maximum length supported by Windows + 1
|
|
initialValueBufLen = 80 // large enough to contain a stringified GUID encoded as UTF-16
|
|
)
|
|
|
|
func logSupportInfo(logf logger.Logf, reason LogSupportInfoReason) {
|
|
var b strings.Builder
|
|
if err := getSupportInfo(&b, reason); err != nil {
|
|
logf("error encoding support info: %v", err)
|
|
return
|
|
}
|
|
logf("%s", b.String())
|
|
}
|
|
|
|
const (
|
|
supportInfoKeyModules = "modules"
|
|
supportInfoKeyRegistry = "registry"
|
|
supportInfoKeyWinsockLSP = "winsockLSP"
|
|
)
|
|
|
|
func getSupportInfo(w io.Writer, reason LogSupportInfoReason) error {
|
|
output := make(map[string]any)
|
|
|
|
regInfo, err := getRegistrySupportInfo(registry.LOCAL_MACHINE, []string{`SOFTWARE\Policies\Tailscale`, winutil.RegBase})
|
|
if err == nil {
|
|
output[supportInfoKeyRegistry] = regInfo
|
|
} else {
|
|
output[supportInfoKeyRegistry] = err
|
|
}
|
|
|
|
if reason == LogSupportInfoReasonBugReport {
|
|
modInfo, err := getModuleInfo()
|
|
if err == nil {
|
|
output[supportInfoKeyModules] = modInfo
|
|
} else {
|
|
output[supportInfoKeyModules] = err
|
|
}
|
|
|
|
lspInfo, err := getWinsockLSPInfo()
|
|
if err == nil {
|
|
output[supportInfoKeyWinsockLSP] = lspInfo
|
|
} else {
|
|
output[supportInfoKeyWinsockLSP] = err
|
|
}
|
|
}
|
|
|
|
enc := json.NewEncoder(w)
|
|
return enc.Encode(output)
|
|
}
|
|
|
|
type getRegistrySupportInfoBufs struct {
|
|
nameBuf []uint16
|
|
valueBuf []byte
|
|
}
|
|
|
|
func getRegistrySupportInfo(root registry.Key, subKeys []string) (map[string]any, error) {
|
|
bufs := getRegistrySupportInfoBufs{
|
|
nameBuf: make([]uint16, maxRegValueNameLen),
|
|
valueBuf: make([]byte, initialValueBufLen),
|
|
}
|
|
|
|
output := make(map[string]any)
|
|
|
|
for _, subKey := range subKeys {
|
|
if err := getRegSubKey(root, subKey, 5, &bufs, output); err != nil && !errors.Is(err, registry.ErrNotExist) {
|
|
return nil, fmt.Errorf("getRegistrySupportInfo: %w", err)
|
|
}
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func keyString(key registry.Key, subKey string) string {
|
|
var keyStr string
|
|
switch key {
|
|
case registry.CLASSES_ROOT:
|
|
keyStr = `HKCR\`
|
|
case registry.CURRENT_USER:
|
|
keyStr = `HKCU\`
|
|
case registry.LOCAL_MACHINE:
|
|
keyStr = `HKLM\`
|
|
case registry.USERS:
|
|
keyStr = `HKU\`
|
|
case registry.CURRENT_CONFIG:
|
|
keyStr = `HKCC\`
|
|
case registry.PERFORMANCE_DATA:
|
|
keyStr = `HKPD\`
|
|
default:
|
|
}
|
|
|
|
return keyStr + subKey
|
|
}
|
|
|
|
func getRegSubKey(key registry.Key, subKey string, recursionLimit int, bufs *getRegistrySupportInfoBufs, output map[string]any) error {
|
|
keyStr := keyString(key, subKey)
|
|
k, err := registry.OpenKey(key, subKey, registry.READ)
|
|
if err != nil {
|
|
return fmt.Errorf("opening %q: %w", keyStr, err)
|
|
}
|
|
defer k.Close()
|
|
|
|
kv := make(map[string]any)
|
|
index := uint32(0)
|
|
|
|
loopValues:
|
|
for {
|
|
nbuf := bufs.nameBuf
|
|
nameLen := uint32(len(nbuf))
|
|
valueType := uint32(0)
|
|
vbuf := bufs.valueBuf
|
|
valueLen := uint32(len(vbuf))
|
|
|
|
err := regEnumValue(k, index, &nbuf[0], &nameLen, nil, &valueType, &vbuf[0], &valueLen)
|
|
switch err {
|
|
case windows.ERROR_NO_MORE_ITEMS:
|
|
break loopValues
|
|
case windows.ERROR_MORE_DATA:
|
|
bufs.valueBuf = make([]byte, valueLen)
|
|
continue
|
|
case nil:
|
|
default:
|
|
return fmt.Errorf("regEnumValue: %w", err)
|
|
}
|
|
|
|
var value any
|
|
|
|
switch valueType {
|
|
case registry.SZ, registry.EXPAND_SZ:
|
|
value = windows.UTF16PtrToString((*uint16)(unsafe.Pointer(&vbuf[0])))
|
|
case registry.BINARY:
|
|
if valueLen > maxBinaryValueLen {
|
|
valueLen = maxBinaryValueLen
|
|
}
|
|
value = append([]byte{}, vbuf[:valueLen]...)
|
|
case registry.DWORD:
|
|
value = binary.LittleEndian.Uint32(vbuf[:4])
|
|
case registry.MULTI_SZ:
|
|
// Adapted from x/sys/windows/registry/(Key).GetStringsValue
|
|
p := (*[1 << 29]uint16)(unsafe.Pointer(&vbuf[0]))[: valueLen/2 : valueLen/2]
|
|
var strs []string
|
|
if len(p) > 0 {
|
|
if p[len(p)-1] == 0 {
|
|
p = p[:len(p)-1]
|
|
}
|
|
strs = make([]string, 0, 5)
|
|
from := 0
|
|
for i, c := range p {
|
|
if c == 0 {
|
|
strs = append(strs, string(utf16.Decode(p[from:i])))
|
|
from = i + 1
|
|
}
|
|
}
|
|
}
|
|
value = strs
|
|
case registry.QWORD:
|
|
value = binary.LittleEndian.Uint64(vbuf[:8])
|
|
default:
|
|
value = fmt.Sprintf("<unsupported value type %d>", valueType)
|
|
}
|
|
|
|
kv[windows.UTF16PtrToString(&nbuf[0])] = value
|
|
index++
|
|
}
|
|
|
|
if recursionLimit > 0 {
|
|
if sks, err := k.ReadSubKeyNames(0); err == nil {
|
|
for _, sk := range sks {
|
|
if err := getRegSubKey(k, sk, recursionLimit-1, bufs, kv); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
output[keyStr] = kv
|
|
return nil
|
|
}
|
|
|
|
type moduleInfo struct {
|
|
path string `json:"-"` // internal use only
|
|
BaseAddress uintptr `json:"baseAddress"`
|
|
Size uint32 `json:"size"`
|
|
DebugInfo map[string]string `json:"debugInfo,omitempty"` // map for JSON marshaling purposes
|
|
DebugInfoErr error `json:"debugInfoErr,omitempty"`
|
|
Signature map[string]string `json:"signature,omitempty"` // map for JSON marshaling purposes
|
|
SignatureErr error `json:"signatureErr,omitempty"`
|
|
VersionInfo map[string]string `json:"versionInfo,omitempty"` // map for JSON marshaling purposes
|
|
VersionErr error `json:"versionErr,omitempty"`
|
|
}
|
|
|
|
func (mi *moduleInfo) setVersionInfo() {
|
|
vi, err := pe.NewVersionInfo(mi.path)
|
|
if err != nil {
|
|
if !errors.Is(err, pe.ErrNotPresent) {
|
|
mi.VersionErr = err
|
|
}
|
|
return
|
|
}
|
|
|
|
info := map[string]string{
|
|
"": vi.VersionNumber().String(),
|
|
}
|
|
|
|
ci, err := vi.Field("CompanyName")
|
|
if err == nil {
|
|
info["companyName"] = ci
|
|
}
|
|
|
|
mi.VersionInfo = info
|
|
}
|
|
|
|
var errAssertingType = errors.New("asserting DataDirectory type")
|
|
|
|
func (mi *moduleInfo) setDebugInfo() {
|
|
pem, err := pe.NewPEFromBaseAddressAndSize(mi.BaseAddress, mi.Size)
|
|
if err != nil {
|
|
mi.DebugInfoErr = err
|
|
return
|
|
}
|
|
defer pem.Close()
|
|
|
|
debugDirAny, err := pem.DataDirectoryEntry(pe.IMAGE_DIRECTORY_ENTRY_DEBUG)
|
|
if err != nil {
|
|
if !errors.Is(err, pe.ErrNotPresent) {
|
|
mi.DebugInfoErr = err
|
|
}
|
|
return
|
|
}
|
|
|
|
debugDir, ok := debugDirAny.([]pe.IMAGE_DEBUG_DIRECTORY)
|
|
if !ok {
|
|
mi.DebugInfoErr = errAssertingType
|
|
return
|
|
}
|
|
|
|
for _, dde := range debugDir {
|
|
if dde.Type != pe.IMAGE_DEBUG_TYPE_CODEVIEW {
|
|
continue
|
|
}
|
|
|
|
cv, err := pem.ExtractCodeViewInfo(dde)
|
|
if err == nil {
|
|
mi.DebugInfo = map[string]string{
|
|
"id": cv.String(),
|
|
"pdb": strings.ToLower(filepath.Base(cv.PDBPath)),
|
|
}
|
|
} else {
|
|
mi.DebugInfoErr = err
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
func (mi *moduleInfo) setAuthenticodeInfo() {
|
|
certSubject, provenance, err := authenticode.QueryCertSubject(mi.path)
|
|
if err != nil {
|
|
if !errors.Is(err, authenticode.ErrSigNotFound) {
|
|
mi.SignatureErr = err
|
|
}
|
|
return
|
|
}
|
|
|
|
sigInfo := map[string]string{
|
|
"subject": certSubject,
|
|
}
|
|
|
|
switch provenance {
|
|
case authenticode.SigProvEmbedded:
|
|
sigInfo["provenance"] = "embedded"
|
|
case authenticode.SigProvCatalog:
|
|
sigInfo["provenance"] = "catalog"
|
|
default:
|
|
}
|
|
|
|
mi.Signature = sigInfo
|
|
}
|
|
|
|
func getModuleInfo() (map[string]moduleInfo, error) {
|
|
// Take a snapshot of all modules currently loaded into the current process
|
|
snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer windows.CloseHandle(snap)
|
|
|
|
result := make(map[string]moduleInfo)
|
|
me := windows.ModuleEntry32{
|
|
Size: uint32(unsafe.Sizeof(windows.ModuleEntry32{})),
|
|
}
|
|
|
|
// Now walk the list
|
|
for merr := windows.Module32First(snap, &me); merr == nil; merr = windows.Module32Next(snap, &me) {
|
|
name := strings.ToLower(windows.UTF16ToString(me.Module[:]))
|
|
path := windows.UTF16ToString(me.ExePath[:])
|
|
base := me.ModBaseAddr
|
|
size := me.ModBaseSize
|
|
|
|
entry := moduleInfo{
|
|
path: path,
|
|
BaseAddress: base,
|
|
Size: size,
|
|
}
|
|
|
|
entry.setVersionInfo()
|
|
entry.setDebugInfo()
|
|
entry.setAuthenticodeInfo()
|
|
|
|
result[name] = entry
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type _WSC_PROVIDER_INFO_TYPE int32
|
|
|
|
const (
|
|
providerInfoLspCategories _WSC_PROVIDER_INFO_TYPE = 0
|
|
)
|
|
|
|
const (
|
|
_SOCKET_ERROR = -1
|
|
)
|
|
|
|
// Note that wsaProtocolInfo needs to be identical to windows.WSAProtocolInfo;
|
|
// the purpose of this type is to have the ability to use it as a reciever in
|
|
// the path and categoryFlags funcs defined below.
|
|
type wsaProtocolInfo windows.WSAProtocolInfo
|
|
|
|
func (pi *wsaProtocolInfo) path() (string, error) {
|
|
var errno int32
|
|
var buf [windows.MAX_PATH]uint16
|
|
bufCount := int32(len(buf))
|
|
ret := wscGetProviderPath(&pi.ProviderId, &buf[0], &bufCount, &errno)
|
|
if ret == _SOCKET_ERROR {
|
|
return "", windows.Errno(errno)
|
|
}
|
|
if ret != 0 {
|
|
return "", errUnexpectedResult
|
|
}
|
|
|
|
return windows.UTF16ToString(buf[:bufCount]), nil
|
|
}
|
|
|
|
func (pi *wsaProtocolInfo) categoryFlags() (uint32, error) {
|
|
var errno int32
|
|
var result uint32
|
|
bufLen := uintptr(unsafe.Sizeof(result))
|
|
ret := wscGetProviderInfo(&pi.ProviderId, providerInfoLspCategories, unsafe.Pointer(&result), &bufLen, 0, &errno)
|
|
if ret == _SOCKET_ERROR {
|
|
return 0, windows.Errno(errno)
|
|
}
|
|
if ret != 0 {
|
|
return 0, errUnexpectedResult
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type wsaProtocolInfoOutput struct {
|
|
Description string `json:"description,omitempty"`
|
|
Version int32 `json:"version"`
|
|
AddressFamily int32 `json:"addressFamily"`
|
|
SocketType int32 `json:"socketType"`
|
|
Protocol int32 `json:"protocol"`
|
|
ServiceFlags1 string `json:"serviceFlags1"`
|
|
ProviderFlags string `json:"providerFlags"`
|
|
Path string `json:"path,omitempty"`
|
|
PathErr error `json:"pathErr,omitempty"`
|
|
Category string `json:"category,omitempty"`
|
|
CategoryErr error `json:"categoryErr,omitempty"`
|
|
BaseProviderID string `json:"baseProviderID,omitempty"`
|
|
LayerProviderID string `json:"layerProviderID,omitempty"`
|
|
Chain []uint32 `json:"chain,omitempty"`
|
|
}
|
|
|
|
func getWinsockLSPInfo() (map[uint32]wsaProtocolInfoOutput, error) {
|
|
protocols, err := enumWinsockProtocols()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make(map[uint32]wsaProtocolInfoOutput, len(protocols))
|
|
for _, p := range protocols {
|
|
v := wsaProtocolInfoOutput{
|
|
Description: windows.UTF16ToString(p.ProtocolName[:]),
|
|
Version: p.Version,
|
|
AddressFamily: p.AddressFamily,
|
|
SocketType: p.SocketType,
|
|
Protocol: p.Protocol,
|
|
ServiceFlags1: fmt.Sprintf("0x%08X", p.ServiceFlags1), // Serializing as hex string to make the flags easier to decode by human inspection
|
|
ProviderFlags: fmt.Sprintf("0x%08X", p.ProviderFlags),
|
|
}
|
|
|
|
switch p.ProtocolChain.ChainLen {
|
|
case windows.BASE_PROTOCOL:
|
|
v.BaseProviderID = p.ProviderId.String()
|
|
case windows.LAYERED_PROTOCOL:
|
|
v.LayerProviderID = p.ProviderId.String()
|
|
default:
|
|
v.Chain = p.ProtocolChain.ChainEntries[:p.ProtocolChain.ChainLen]
|
|
}
|
|
|
|
// Queries that are only valid for base and layered protocols (not chains)
|
|
if v.Chain == nil {
|
|
path, err := p.path()
|
|
if err == nil {
|
|
v.Path = strings.ToLower(path)
|
|
} else {
|
|
v.PathErr = err
|
|
}
|
|
|
|
category, err := p.categoryFlags()
|
|
if err == nil {
|
|
v.Category = fmt.Sprintf("0x%08X", category)
|
|
} else if !errors.Is(err, windows.WSAEINVALIDPROVIDER) {
|
|
// WSAEINVALIDPROVIDER == "no category info found", so we only log
|
|
// errors other than that one.
|
|
v.CategoryErr = err
|
|
}
|
|
}
|
|
|
|
// Chains reference other providers using catalog entry IDs, so we use that
|
|
// value as the key in our map.
|
|
result[p.CatalogEntryId] = v
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func enumWinsockProtocols() ([]wsaProtocolInfo, error) {
|
|
// Get the required size
|
|
var errno int32
|
|
var bytesReqd uint32
|
|
ret := wscEnumProtocols(nil, nil, &bytesReqd, &errno)
|
|
if ret != _SOCKET_ERROR {
|
|
return nil, errUnexpectedResult
|
|
}
|
|
if e := windows.Errno(errno); e != windows.WSAENOBUFS {
|
|
return nil, e
|
|
}
|
|
|
|
// Allocate
|
|
szEntry := uint32(unsafe.Sizeof(wsaProtocolInfo{}))
|
|
buf := make([]wsaProtocolInfo, bytesReqd/szEntry)
|
|
|
|
// Now do the query for real
|
|
bufLen := uint32(len(buf)) * szEntry
|
|
ret = wscEnumProtocols(nil, &buf[0], &bufLen, &errno)
|
|
if ret == _SOCKET_ERROR {
|
|
return nil, windows.Errno(errno)
|
|
}
|
|
|
|
return buf, nil
|
|
}
|