netstat, portlist: update Windows implementation to disambiguate svchost processes

We change our invocations of GetExtendedTcpTable to request additional
information about the "module" responsible for the port. In addition to pid,
this output also includes sufficient metadata to enable Windows to resolve
process names and disambiguate svchost processes.

We store the OS-specific output in an OSMetadata field in netstat.Entry, which
portlist may then use as necessary to actually resolve the process/module name.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
pull/6794/head
Aaron Klotz 2 years ago
parent a06217a8bd
commit 296f53524c

@ -17,6 +17,7 @@ type Entry struct {
Local, Remote netip.AddrPort Local, Remote netip.AddrPort
Pid int Pid int
State string // TODO: type? State string // TODO: type?
OSMetadata OSMetadata
} }
// Table contains local machine's TCP connection entries. // Table contains local machine's TCP connection entries.

@ -6,6 +6,10 @@
package netstat package netstat
// OSMetadata includes any additional OS-specific information that may be
// obtained during the retrieval of a given Entry.
type OSMetadata struct{}
func get() (*Table, error) { func get() (*Table, error) {
return nil, ErrNotImplemented return nil, ErrNotImplemented
} }

@ -10,7 +10,6 @@ import (
"fmt" "fmt"
"math/bits" "math/bits"
"net/netip" "net/netip"
"syscall"
"unsafe" "unsafe"
"github.com/josharian/native" "github.com/josharian/native"
@ -18,37 +17,99 @@ import (
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
) )
// OSMetadata includes any additional OS-specific information that may be
// obtained during the retrieval of a given Entry.
type OSMetadata interface {
GetModule() (string, error)
}
// See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable // See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
// TCP_TABLE_OWNER_PID_ALL means to include the PID info. The table type // TCP_TABLE_OWNER_MODULE_ALL means to include the PID and module. The table type
// we get back from Windows depends on AF_INET vs AF_INET6: // we get back from Windows depends on AF_INET vs AF_INET6:
// MIB_TCPTABLE_OWNER_PID for v4 or MIB_TCP6TABLE_OWNER_PID for v6. // MIB_TCPTABLE_OWNER_MODULE for v4 or MIB_TCP6TABLE_OWNER_MODULE for v6.
const tcpTableOwnerPidAll = 5 const tcpTableOwnerModuleAll = 8
// TCPIP_OWNER_MODULE_BASIC_INFO means to request "basic information" about the
// owner module.
const tcpipOwnerModuleBasicInfo = 0
var ( var (
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll") iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
getTCPTable = iphlpapi.NewProc("GetExtendedTcpTable") getTCPTable = iphlpapi.NewProc("GetExtendedTcpTable")
getOwnerModuleFromTcpEntry = iphlpapi.NewProc("GetOwnerModuleFromTcpEntry")
getOwnerModuleFromTcp6Entry = iphlpapi.NewProc("GetOwnerModuleFromTcp6Entry")
// TODO: GetExtendedUdpTable also? if/when needed. // TODO: GetExtendedUdpTable also? if/when needed.
) )
type _MIB_TCPROW_OWNER_PID struct { // See https://web.archive.org/web/20221219211913/https://learn.microsoft.com/en-us/windows/win32/api/tcpmib/ns-tcpmib-mib_tcprow_owner_module
state uint32 type _MIB_TCPROW_OWNER_MODULE struct {
localAddr uint32 state uint32
localPort uint32 localAddr uint32
remoteAddr uint32 localPort uint32
remotePort uint32 remoteAddr uint32
pid uint32 remotePort uint32
pid uint32
createTimestamp int64
owningModuleInfo [16]uint64
}
func (row *_MIB_TCPROW_OWNER_MODULE) asEntry() Entry {
return Entry{
Local: ipport4(row.localAddr, port(&row.localPort)),
Remote: ipport4(row.remoteAddr, port(&row.remotePort)),
Pid: int(row.pid),
State: state(row.state),
OSMetadata: row,
}
}
type _MIB_TCPTABLE_OWNER_MODULE struct {
numEntries uint32
table _MIB_TCPROW_OWNER_MODULE
}
func (m *_MIB_TCPTABLE_OWNER_MODULE) getRows() []_MIB_TCPROW_OWNER_MODULE {
return unsafe.Slice(&m.table, m.numEntries)
}
// See https://web.archive.org/web/20221219212442/https://learn.microsoft.com/en-us/windows/win32/api/tcpmib/ns-tcpmib-mib_tcp6row_owner_module
type _MIB_TCP6ROW_OWNER_MODULE struct {
localAddr [16]byte
localScope uint32
localPort uint32
remoteAddr [16]byte
remoteScope uint32
remotePort uint32
state uint32
pid uint32
createTimestamp int64
owningModuleInfo [16]uint64
}
func (row *_MIB_TCP6ROW_OWNER_MODULE) asEntry() Entry {
return Entry{
Local: ipport6(row.localAddr, row.localScope, port(&row.localPort)),
Remote: ipport6(row.remoteAddr, row.remoteScope, port(&row.remotePort)),
Pid: int(row.pid),
State: state(row.state),
OSMetadata: row,
}
}
type _MIB_TCP6TABLE_OWNER_MODULE struct {
numEntries uint32
table _MIB_TCP6ROW_OWNER_MODULE
} }
type _MIB_TCP6ROW_OWNER_PID struct { func (m *_MIB_TCP6TABLE_OWNER_MODULE) getRows() []_MIB_TCP6ROW_OWNER_MODULE {
localAddr [16]byte return unsafe.Slice(&m.table, m.numEntries)
localScope uint32 }
localPort uint32
remoteAddr [16]byte // See https://web.archive.org/web/20221219213143/https://learn.microsoft.com/en-us/windows/win32/api/iprtrmib/ns-iprtrmib-tcpip_owner_module_basic_info
remoteScope uint32 type _TCPIP_OWNER_MODULE_BASIC_INFO struct {
remotePort uint32 moduleName *uint16
state uint32 modulePath *uint16
pid uint32
} }
func get() (*Table, error) { func get() (*Table, error) {
@ -72,13 +133,13 @@ func (t *Table) addEntries(fam int) error {
uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&size)),
1, // sorted 1, // sorted
uintptr(fam), uintptr(fam),
tcpTableOwnerPidAll, tcpTableOwnerModuleAll,
0, // reserved; "must be zero" 0, // reserved; "must be zero"
) )
if err == 0 { if err == 0 {
break break
} }
if err == uintptr(syscall.ERROR_INSUFFICIENT_BUFFER) { if err == uintptr(windows.ERROR_INSUFFICIENT_BUFFER) {
const maxSize = 10 << 20 const maxSize = 10 << 20
if size > maxSize || size < 4 { if size > maxSize || size < 4 {
return fmt.Errorf("unreasonable kernel-reported size %d", size) return fmt.Errorf("unreasonable kernel-reported size %d", size)
@ -87,48 +148,28 @@ func (t *Table) addEntries(fam int) error {
addr = unsafe.Pointer(&buf[0]) addr = unsafe.Pointer(&buf[0])
continue continue
} }
return syscall.Errno(err) return windows.Errno(err)
} }
if len(buf) < int(size) { if len(buf) < int(size) {
return errors.New("unexpected size growth from system call") return errors.New("unexpected size growth from system call")
} }
buf = buf[:size] buf = buf[:size]
numEntries := native.Endian.Uint32(buf[:4])
buf = buf[4:]
var recSize int
switch fam { switch fam {
case windows.AF_INET: case windows.AF_INET:
recSize = 6 * 4 info := (*_MIB_TCPTABLE_OWNER_MODULE)(unsafe.Pointer(&buf[0]))
rows := info.getRows()
for _, row := range rows {
t.Entries = append(t.Entries, row.asEntry())
}
case windows.AF_INET6: case windows.AF_INET6:
recSize = 6*4 + 16*2 info := (*_MIB_TCP6TABLE_OWNER_MODULE)(unsafe.Pointer(&buf[0]))
} rows := info.getRows()
dataLen := numEntries * uint32(recSize) for _, row := range rows {
if uint32(len(buf)) > dataLen { t.Entries = append(t.Entries, row.asEntry())
buf = buf[:dataLen]
}
for len(buf) >= recSize {
switch fam {
case windows.AF_INET:
row := (*_MIB_TCPROW_OWNER_PID)(unsafe.Pointer(&buf[0]))
t.Entries = append(t.Entries, Entry{
Local: ipport4(row.localAddr, port(&row.localPort)),
Remote: ipport4(row.remoteAddr, port(&row.remotePort)),
Pid: int(row.pid),
State: state(row.state),
})
case windows.AF_INET6:
row := (*_MIB_TCP6ROW_OWNER_PID)(unsafe.Pointer(&buf[0]))
t.Entries = append(t.Entries, Entry{
Local: ipport6(row.localAddr, row.localScope, port(&row.localPort)),
Remote: ipport6(row.remoteAddr, row.remoteScope, port(&row.remotePort)),
Pid: int(row.pid),
State: state(row.state),
})
} }
buf = buf[recSize:]
} }
return nil return nil
} }
@ -178,3 +219,43 @@ func port(v *uint32) uint16 {
} }
return uint16(*v >> 16) return uint16(*v >> 16)
} }
type moduleInfoConstraint interface {
_MIB_TCPROW_OWNER_MODULE | _MIB_TCP6ROW_OWNER_MODULE
}
func moduleInfo[entryType moduleInfoConstraint](entry *entryType, proc *windows.LazyProc) (string, error) {
var buf []byte
var desiredLen uint32
var addr unsafe.Pointer
for {
e, _, _ := proc.Call(
uintptr(unsafe.Pointer(entry)),
uintptr(tcpipOwnerModuleBasicInfo),
uintptr(addr),
uintptr(unsafe.Pointer(&desiredLen)),
)
err := windows.Errno(e)
if err == windows.ERROR_SUCCESS {
break
}
if err != windows.ERROR_INSUFFICIENT_BUFFER {
return "", err
}
buf = make([]byte, desiredLen)
addr = unsafe.Pointer(&buf[0])
}
basicInfo := (*_TCPIP_OWNER_MODULE_BASIC_INFO)(addr)
return windows.UTF16PtrToString(basicInfo.moduleName), nil
}
func (m *_MIB_TCPROW_OWNER_MODULE) GetModule() (string, error) {
return moduleInfo(m, getOwnerModuleFromTcpEntry)
}
func (m *_MIB_TCP6ROW_OWNER_MODULE) GetModule() (string, error) {
return moduleInfo(m, getOwnerModuleFromTcp6Entry)
}

@ -5,12 +5,8 @@
package portlist package portlist
import ( import (
"path/filepath"
"strings"
"syscall"
"time" "time"
"golang.org/x/sys/windows"
"tailscale.com/net/netstat" "tailscale.com/net/netstat"
) )
@ -26,7 +22,7 @@ func init() {
type famPort struct { type famPort struct {
proto string proto string
port uint16 port uint16
pid uintptr pid uint32
} }
type windowsImpl struct { type windowsImpl struct {
@ -69,19 +65,25 @@ func (im *windowsImpl) AppendListeningPorts(base []Port) ([]Port, error) {
fp := famPort{ fp := famPort{
proto: "tcp", // TODO(bradfitz): UDP too; add to netstat proto: "tcp", // TODO(bradfitz): UDP too; add to netstat
port: e.Local.Port(), port: e.Local.Port(),
pid: uintptr(e.Pid), pid: uint32(e.Pid),
} }
pm, ok := im.known[fp] pm, ok := im.known[fp]
if ok { if ok {
pm.keep = true pm.keep = true
continue continue
} }
var process string
if e.OSMetadata != nil {
if module, err := e.OSMetadata.GetModule(); err == nil {
process = module
}
}
pm = &portMeta{ pm = &portMeta{
keep: true, keep: true,
port: Port{ port: Port{
Proto: "tcp", Proto: "tcp",
Port: e.Local.Port(), Port: e.Local.Port(),
Process: procNameOfPid(e.Pid), Process: process,
}, },
} }
im.known[fp] = pm im.known[fp] = pm
@ -94,27 +96,6 @@ func (im *windowsImpl) AppendListeningPorts(base []Port) ([]Port, error) {
} }
ret = append(ret, m.port) ret = append(ret, m.port)
} }
return sortAndDedup(ret), nil
}
func procNameOfPid(pid int) string { return sortAndDedup(ret), nil
const da = windows.PROCESS_QUERY_LIMITED_INFORMATION
h, err := syscall.OpenProcess(da, false, uint32(pid))
if err != nil {
return ""
}
defer syscall.CloseHandle(h)
var buf [512]uint16
var size = uint32(len(buf))
if err := windows.QueryFullProcessImageName(windows.Handle(h), 0, &buf[0], &size); err != nil {
return ""
}
name := filepath.Base(windows.UTF16ToString(buf[:]))
if name == "." {
return ""
}
name = strings.TrimSuffix(name, ".exe")
name = strings.TrimSuffix(name, ".EXE")
return name
} }

Loading…
Cancel
Save