mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-01 22:12:31 +00:00
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>
This commit is contained in:
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
|
||||||
}
|
}
|
||||||
|
|
||||||
type _MIB_TCP6ROW_OWNER_PID struct {
|
func (row *_MIB_TCPROW_OWNER_MODULE) asEntry() Entry {
|
||||||
localAddr [16]byte
|
return Entry{
|
||||||
localScope uint32
|
Local: ipport4(row.localAddr, port(&row.localPort)),
|
||||||
localPort uint32
|
Remote: ipport4(row.remoteAddr, port(&row.remotePort)),
|
||||||
remoteAddr [16]byte
|
Pid: int(row.pid),
|
||||||
remoteScope uint32
|
State: state(row.state),
|
||||||
remotePort uint32
|
OSMetadata: row,
|
||||||
state uint32
|
}
|
||||||
pid uint32
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *_MIB_TCP6TABLE_OWNER_MODULE) getRows() []_MIB_TCP6ROW_OWNER_MODULE {
|
||||||
|
return unsafe.Slice(&m.table, m.numEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://web.archive.org/web/20221219213143/https://learn.microsoft.com/en-us/windows/win32/api/iprtrmib/ns-iprtrmib-tcpip_owner_module_basic_info
|
||||||
|
type _TCPIP_OWNER_MODULE_BASIC_INFO struct {
|
||||||
|
moduleName *uint16
|
||||||
|
modulePath *uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
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]))
|
||||||
case windows.AF_INET6:
|
rows := info.getRows()
|
||||||
recSize = 6*4 + 16*2
|
for _, row := range rows {
|
||||||
}
|
t.Entries = append(t.Entries, row.asEntry())
|
||||||
dataLen := numEntries * uint32(recSize)
|
}
|
||||||
if uint32(len(buf)) > dataLen {
|
case windows.AF_INET6:
|
||||||
buf = buf[:dataLen]
|
info := (*_MIB_TCP6TABLE_OWNER_MODULE)(unsafe.Pointer(&buf[0]))
|
||||||
}
|
rows := info.getRows()
|
||||||
for len(buf) >= recSize {
|
for _, row := range rows {
|
||||||
switch fam {
|
t.Entries = append(t.Entries, row.asEntry())
|
||||||
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
|
return sortAndDedup(ret), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func procNameOfPid(pid int) string {
|
|
||||||
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…
x
Reference in New Issue
Block a user