diff --git a/pkg/config/setup/config_test.go b/pkg/config/setup/config_test.go index a52c698fb2e68..7c321000fe55e 100644 --- a/pkg/config/setup/config_test.go +++ b/pkg/config/setup/config_test.go @@ -1448,3 +1448,11 @@ app_key: '***********************************acccc' yet_another_key: "********"` assert.YAMLEq(t, expected, scrubbed) } + +func TestAllKeysLowercased(t *testing.T) { + for _, key := range Datadog().AllKeysLowercased() { + splitKey := strings.Split(key, ".") + keyName := splitKey[len(splitKey)-1] + t.Logf("Key: %s", keyName) + } +} diff --git a/pkg/diagnose/ports/ports.go b/pkg/diagnose/ports/ports.go index 17db62bf8f801..a3b387d27705c 100644 --- a/pkg/diagnose/ports/ports.go +++ b/pkg/diagnose/ports/ports.go @@ -9,6 +9,7 @@ package ports import ( "fmt" "path" + "runtime" "strings" pkgconfigsetup "github.com/DataDog/datadog-agent/pkg/config/setup" @@ -19,7 +20,7 @@ import ( var agentNames = map[string]struct{}{ "datadog-agent": {}, "agent": {}, "trace-agent": {}, "process-agent": {}, "system-probe": {}, "security-agent": {}, - "dogstatsd": {}, + "dogstatsd": {}, "agent.exe": {}, "process-agent.exe": {}, "trace-agent.exe": {}, } // DiagnosePortSuite displays information about the ports used in the agent configuration @@ -40,6 +41,11 @@ func DiagnosePortSuite() []diagnosis.Diagnosis { var diagnoses []diagnosis.Diagnosis for _, key := range pkgconfigsetup.Datadog().AllKeysLowercased() { + // on windows, we skip the ports used by apm agent and process agent because the core agent does not have permissions to retrieve proc name + if runtime.GOOS == "windows" && (strings.HasPrefix(key, "apm_config") || strings.HasPrefix(key, "process_config")) { + continue + } + splitKey := strings.Split(key, ".") keyName := splitKey[len(splitKey)-1] if keyName != "port" && !strings.HasPrefix(keyName, "port_") && !strings.HasSuffix(keyName, "_port") { @@ -76,8 +82,18 @@ func DiagnosePortSuite() []diagnosis.Diagnosis { if port.Pid == 0 { diagnoses = append(diagnoses, diagnosis.Diagnosis{ Name: key, - Result: diagnosis.DiagnosisFail, - Diagnosis: fmt.Sprintf("Required port %d is already used by an another process.", value), + Result: diagnosis.DiagnosisWarning, + Diagnosis: fmt.Sprintf("Required port %d is already used by an another process. Verify the process that is using this port is an Agent process.", value), + }) + continue + } + + // on windows, if the port is used by a process that is not 'agent.exe', we cannot retrieve the proc name + if port.Process == "" && port.Pid != 0 { + diagnoses = append(diagnoses, diagnosis.Diagnosis{ + Name: key, + Result: diagnosis.DiagnosisWarning, + Diagnosis: fmt.Sprintf("Required port %d is already used by an another process (PID=%d). Verify that the process that is using this port is an Agent process.", value, port.Pid), }) continue } diff --git a/pkg/diagnose/runner.go b/pkg/diagnose/runner.go index 281860310689a..160a667b65d80 100644 --- a/pkg/diagnose/runner.go +++ b/pkg/diagnose/runner.go @@ -11,7 +11,6 @@ import ( "fmt" "io" "regexp" - "runtime" "sort" "strings" @@ -522,8 +521,5 @@ func RegisterConnectivityDatadogEventPlatform(catalog *diagnosis.Catalog) { // RegisterPortConflict registers the port-conflict diagnose suite. func RegisterPortConflict(catalog *diagnosis.Catalog) { - // port-conflict suite available in darwin and linux only for now - if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { - catalog.Register("port-conflict", func() []diagnosis.Diagnosis { return ports.DiagnosePortSuite() }) - } + catalog.Register("port-conflict", func() []diagnosis.Diagnosis { return ports.DiagnosePortSuite() }) } diff --git a/pkg/util/port/portlist/netstat.go b/pkg/util/port/portlist/netstat.go index b79f100f9055b..1be223e078e21 100644 --- a/pkg/util/port/portlist/netstat.go +++ b/pkg/util/port/portlist/netstat.go @@ -3,132 +3,31 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2014-present Datadog, Inc. -//go:build darwin - package portlist import ( - "bufio" - "bytes" - "io" - - "go4.org/mem" + "net/netip" ) -// parsePort returns the port number at the end of s following the last "." or -// ":", whichever comes last. It returns -1 on a parse error or invalid number -// and 0 if the port number was "*". -// -// This is basically net.SplitHostPort except that it handles a "." (as macOS -// and others return in netstat output), uses mem.RO, and validates that the -// port must be numeric and in the uint16 range. -func parsePort(s mem.RO) int { - // a.b.c.d:1234 or [a:b:c:d]:1234 - i1 := mem.LastIndexByte(s, ':') - // a.b.c.d.1234 or [a:b:c:d].1234 - i2 := mem.LastIndexByte(s, '.') - - i := i1 - if i2 > i { - i = i2 - } - if i < 0 { - // no match; weird - return -1 - } - - portstr := s.SliceFrom(i + 1) - if portstr.EqualString("*") { - return 0 - } - - port, err := mem.ParseUint(portstr, 10, 16) - if err != nil { - // invalid port; weird - return -1 - } - - return int(port) +// Entry is a single entry in the connection table. +type Entry struct { + Local, Remote netip.AddrPort + Pid int + State string + OSMetadata OSMetadata } -func isLoopbackAddr(s mem.RO) bool { - return mem.HasPrefix(s, mem.S("127.")) || - mem.HasPrefix(s, mem.S("[::1]:")) || - mem.HasPrefix(s, mem.S("::1.")) +// Table contains local machine's TCP connection entries. +// +// Currently only TCP (IPv4 and IPv6) are included. +type Table struct { + Entries []Entry } -// appendParsePortsNetstat appends to base listening ports -// from "netstat" output, read from br. See TestParsePortsNetstat -// for example input lines. +// GetConnTable returns the connection table. // -// This used to be a lowest common denominator parser for "netstat -na" format. -// All of Linux, Windows, and macOS support -na and give similar-ish output -// formats that we can parse without special detection logic. -// Unfortunately, options to filter by proto or state are non-portable, -// so we'll filter for ourselves. -// Nowadays, though, we only use it for macOS as of 2022-11-04. -func appendParsePortsNetstat(base []Port, br *bufio.Reader, includeLocalhost bool) ([]Port, error) { - ret := base - var fieldBuf [10]mem.RO - for { - line, err := br.ReadBytes('\n') - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - trimline := bytes.TrimSpace(line) - cols := mem.AppendFields(fieldBuf[:0], mem.B(trimline)) - if len(cols) < 1 { - continue - } - protos := cols[0] - - var proto string - var laddr, raddr mem.RO - if mem.HasPrefixFold(protos, mem.S("tcp")) { - if len(cols) < 4 { - continue - } - proto = "tcp" - laddr = cols[len(cols)-3] - raddr = cols[len(cols)-2] - state := cols[len(cols)-1] - if !mem.HasPrefix(state, mem.S("LISTEN")) { - // not interested in non-listener sockets - continue - } - if !includeLocalhost && isLoopbackAddr(laddr) { - // not interested in loopback-bound listeners - continue - } - } else if mem.HasPrefixFold(protos, mem.S("udp")) { - if len(cols) < 3 { - continue - } - proto = "udp" - laddr = cols[len(cols)-2] - raddr = cols[len(cols)-1] - if !includeLocalhost && isLoopbackAddr(laddr) { - // not interested in loopback-bound listeners - continue - } - } else { - // not interested in other protocols - continue - } - - lport := parsePort(laddr) - rport := parsePort(raddr) - if rport > 0 || lport <= 0 { - // not interested in "connected" sockets - continue - } - ret = append(ret, Port{ - Proto: proto, - Port: uint16(lport), - }) - } - return ret, nil +// It returns ErrNotImplemented if the table is not available for the +// current operating system. +func GetConnTable() (*Table, error) { + return getConnTable() } diff --git a/pkg/util/port/portlist/netstat_macos.go b/pkg/util/port/portlist/netstat_macos.go new file mode 100644 index 0000000000000..838f7bdff70e9 --- /dev/null +++ b/pkg/util/port/portlist/netstat_macos.go @@ -0,0 +1,148 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build darwin + +package portlist + +import ( + "bufio" + "bytes" + "errors" + "io" + + "go4.org/mem" +) + +// ErrNotImplemented is the "not implemented" error given by `gopsutil` when an +// OS doesn't support and API. Unfortunately it's in an internal package so +// we can't import it so we'll copy it here. +var ErrNotImplemented = errors.New("not implemented yet") + +// OSMetadata includes any additional OS-specific information that may be +// obtained during the retrieval of a given Entry. +type OSMetadata struct{} + +// parsePort returns the port number at the end of s following the last "." or +// ":", whichever comes last. It returns -1 on a parse error or invalid number +// and 0 if the port number was "*". +// +// This is basically net.SplitHostPort except that it handles a "." (as macOS +// and others return in netstat output), uses mem.RO, and validates that the +// port must be numeric and in the uint16 range. +func parsePort(s mem.RO) int { + // a.b.c.d:1234 or [a:b:c:d]:1234 + i1 := mem.LastIndexByte(s, ':') + // a.b.c.d.1234 or [a:b:c:d].1234 + i2 := mem.LastIndexByte(s, '.') + + i := i1 + if i2 > i { + i = i2 + } + if i < 0 { + // no match; weird + return -1 + } + + portstr := s.SliceFrom(i + 1) + if portstr.EqualString("*") { + return 0 + } + + port, err := mem.ParseUint(portstr, 10, 16) + if err != nil { + // invalid port; weird + return -1 + } + + return int(port) +} + +func isLoopbackAddr(s mem.RO) bool { + return mem.HasPrefix(s, mem.S("127.")) || + mem.HasPrefix(s, mem.S("[::1]:")) || + mem.HasPrefix(s, mem.S("::1.")) +} + +// appendParsePortsNetstat appends to base listening ports +// from "netstat" output, read from br. See TestParsePortsNetstat +// for example input lines. +// +// This used to be a lowest common denominator parser for "netstat -na" format. +// All of Linux, Windows, and macOS support -na and give similar-ish output +// formats that we can parse without special detection logic. +// Unfortunately, options to filter by proto or state are non-portable, +// so we'll filter for ourselves. +// Nowadays, though, we only use it for macOS as of 2022-11-04. +func appendParsePortsNetstat(base []Port, br *bufio.Reader, includeLocalhost bool) ([]Port, error) { + ret := base + var fieldBuf [10]mem.RO + for { + line, err := br.ReadBytes('\n') + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + trimline := bytes.TrimSpace(line) + cols := mem.AppendFields(fieldBuf[:0], mem.B(trimline)) + if len(cols) < 1 { + continue + } + protos := cols[0] + + var proto string + var laddr, raddr mem.RO + if mem.HasPrefixFold(protos, mem.S("tcp")) { + if len(cols) < 4 { + continue + } + proto = "tcp" + laddr = cols[len(cols)-3] + raddr = cols[len(cols)-2] + state := cols[len(cols)-1] + if !mem.HasPrefix(state, mem.S("LISTEN")) { + // not interested in non-listener sockets + continue + } + if !includeLocalhost && isLoopbackAddr(laddr) { + // not interested in loopback-bound listeners + continue + } + } else if mem.HasPrefixFold(protos, mem.S("udp")) { + if len(cols) < 3 { + continue + } + proto = "udp" + laddr = cols[len(cols)-2] + raddr = cols[len(cols)-1] + if !includeLocalhost && isLoopbackAddr(laddr) { + // not interested in loopback-bound listeners + continue + } + } else { + // not interested in other protocols + continue + } + + lport := parsePort(laddr) + rport := parsePort(raddr) + if rport > 0 || lport <= 0 { + // not interested in "connected" sockets + continue + } + ret = append(ret, Port{ + Proto: proto, + Port: uint16(lport), + }) + } + return ret, nil +} + +func getConnTable() (*Table, error) { + return nil, ErrNotImplemented +} diff --git a/pkg/util/port/portlist/netstat_noimpl.go b/pkg/util/port/portlist/netstat_noimpl.go new file mode 100644 index 0000000000000..eeef1b4de430b --- /dev/null +++ b/pkg/util/port/portlist/netstat_noimpl.go @@ -0,0 +1,23 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2014-present Datadog, Inc. + +//go:build !windows && !darwin + +package portlist + +import "errors" + +// ErrNotImplemented is the "not implemented" error given by `gopsutil` when an +// OS doesn't support and API. Unfortunately it's in an internal package so +// we can't import it so we'll copy it here. +var ErrNotImplemented = errors.New("not implemented yet") + +// OSMetadata includes any additional OS-specific information that may be +// obtained during the retrieval of a given Entry. +type OSMetadata struct{} + +func getConnTable() (*Table, error) { + return nil, ErrNotImplemented +} diff --git a/pkg/util/port/portlist/netstat_windows.go b/pkg/util/port/portlist/netstat_windows.go new file mode 100644 index 0000000000000..63104881f1ea6 --- /dev/null +++ b/pkg/util/port/portlist/netstat_windows.go @@ -0,0 +1,283 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build windows + +package portlist + +import ( + "errors" + "fmt" + "math/bits" + "net/netip" + "unsafe" + + "golang.org/x/sys/cpu" + "golang.org/x/sys/windows" +) + +// OSMetadata includes any additional OS-specific information that may be +// obtained during the retrieval of a given Entry. +type OSMetadata interface { + // GetModule returns the entry's module name. + // + // It returns ("", nil) if no entry is found. As of 2023-01-27, any returned + // error is silently discarded by its sole caller in portlist_windows.go and + // treated equivalently as returning ("", nil), but this may change in the + // future. An error should only be returned in casees that are worthy of + // being logged at least. + GetModule() (string, error) +} + +// See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable + +// 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: +// MIB_TCPTABLE_OWNER_MODULE for v4 or MIB_TCP6TABLE_OWNER_MODULE for v6. +const tcpTableOwnerModuleAll = 8 + +// TCPIP_OWNER_MODULE_BASIC_INFO means to request "basic information" about the +// owner module. +const tcpipOwnerModuleBasicInfo = 0 + +var ( + iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + getTCPTable = iphlpapi.NewProc("GetExtendedTcpTable") + getOwnerModuleFromTCPEntry = iphlpapi.NewProc("GetOwnerModuleFromTcpEntry") + getOwnerModuleFromTCP6Entry = iphlpapi.NewProc("GetOwnerModuleFromTcp6Entry") + // TODO: GetExtendedUdpTable also? if/when needed. +) + +type _MIB_TCPROW_OWNER_MODULE struct { //nolint:revive // Windows API type + state uint32 + localAddr uint32 + localPort uint32 + remoteAddr uint32 + remotePort uint32 + pid uint32 + createTimestamp int64 + owningModuleInfo [16]uint64 +} + +func ipport4(addr uint32, port uint16) netip.AddrPort { + if !cpu.IsBigEndian { + addr = bits.ReverseBytes32(addr) + } + return netip.AddrPortFrom( + netip.AddrFrom4([4]byte{byte(addr >> 24), byte(addr >> 16), byte(addr >> 8), byte(addr)}), + port) +} + +func port(v *uint32) uint16 { + if !cpu.IsBigEndian { + return uint16(bits.ReverseBytes32(*v) >> 16) + } + return uint16(*v >> 16) +} + +var states = []string{ + "", + "CLOSED", + "LISTEN", + "SYN-SENT", + "SYN-RECEIVED", + "ESTABLISHED", + "FIN-WAIT-1", + "FIN-WAIT-2", + "CLOSE-WAIT", + "CLOSING", + "LAST-ACK", + "DELETE-TCB", +} + +func state(v uint32) string { + if v < uint32(len(states)) { + return states[v] + } + return fmt.Sprintf("unknown-state-%d", v) +} + +// 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 { //nolint:revive // Windows API type + moduleName *uint16 + modulePath *uint16 +} + +type moduleInfoConstraint interface { + _MIB_TCPROW_OWNER_MODULE | _MIB_TCP6ROW_OWNER_MODULE +} + +// 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 { //nolint:revive // Windows API type + localAddr [16]byte + localScope uint32 + localPort uint32 + remoteAddr [16]byte + remoteScope uint32 + remotePort uint32 + state uint32 + pid uint32 + createTimestamp int64 + owningModuleInfo [16]uint64 +} + +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_NOT_FOUND { + return "", nil + } + if err != windows.ERROR_INSUFFICIENT_BUFFER { + return "", err + } + if desiredLen > 1<<20 { + // Sanity check before allocating too much + return "", nil + } + buf = make([]byte, desiredLen) + addr = unsafe.Pointer(&buf[0]) + } + if addr == nil { + // GetOwnerModuleFromTcp*Entry can apparently return ERROR_SUCCESS + // (NO_ERROR) on the first call without the usual first + // ERROR_INSUFFICIENT_BUFFER result. Windows said success, so interpret + // that was successfully not having data. + return "", nil + } + basicInfo := (*_TCPIP_OWNER_MODULE_BASIC_INFO)(addr) + return windows.UTF16PtrToString(basicInfo.moduleName), nil +} + +// GetModule implements OSMetaData +func (row *_MIB_TCPROW_OWNER_MODULE) GetModule() (string, error) { + return moduleInfo(row, getOwnerModuleFromTCPEntry) +} + +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_TCP6TABLE_OWNER_MODULE struct { //nolint:revive // Windows API type + 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) +} + +func ipport6(addr [16]byte, scope uint32, port uint16) netip.AddrPort { + ip := netip.AddrFrom16(addr).Unmap() + if scope != 0 { + // TODO: something better here? + ip = ip.WithZone(fmt.Sprint(scope)) + } + return netip.AddrPortFrom(ip, port) +} + +// GetModule implements OSMetadata. +func (row *_MIB_TCP6ROW_OWNER_MODULE) GetModule() (string, error) { + return moduleInfo(row, getOwnerModuleFromTCP6Entry) +} + +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_TCPTABLE_OWNER_MODULE struct { //nolint:revive // Windows API type + 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) +} + +func (t *Table) addEntries(fam int) error { + var size uint32 + var addr unsafe.Pointer + var buf []byte + for { + err, _, _ := getTCPTable.Call( + uintptr(addr), + uintptr(unsafe.Pointer(&size)), + 1, // sorted + uintptr(fam), + tcpTableOwnerModuleAll, + 0, // reserved; "must be zero" + ) + if err == 0 { + break + } + if err == uintptr(windows.ERROR_INSUFFICIENT_BUFFER) { + const maxSize = 10 << 20 + if size > maxSize || size < 4 { + return fmt.Errorf("unreasonable kernel-reported size %d", size) + } + buf = make([]byte, size) + addr = unsafe.Pointer(&buf[0]) + continue + } + return windows.Errno(err) + } + if len(buf) < int(size) { + return errors.New("unexpected size growth from system call") + } + buf = buf[:size] + + switch fam { + case windows.AF_INET: + 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: + info := (*_MIB_TCP6TABLE_OWNER_MODULE)(unsafe.Pointer(&buf[0])) + rows := info.getRows() + for _, row := range rows { + t.Entries = append(t.Entries, row.asEntry()) + } + } + + return nil +} + +func getConnTable() (*Table, error) { + t := new(Table) + if err := t.addEntries(windows.AF_INET); err != nil { + return nil, fmt.Errorf("failed to get IPv4 entries: %w", err) + } + if err := t.addEntries(windows.AF_INET6); err != nil { + return nil, fmt.Errorf("failed to get IPv6 entries: %w", err) + } + return t, nil +} diff --git a/pkg/util/port/portlist/poller_windows.go b/pkg/util/port/portlist/poller_windows.go index ec33188b84ddf..c009dd654b1d1 100644 --- a/pkg/util/port/portlist/poller_windows.go +++ b/pkg/util/port/portlist/poller_windows.go @@ -18,21 +18,88 @@ var ErrNotImplemented = errors.New("not implemented yet") // init initializes the Poller by ensuring it has an underlying func (p *Poller) init() { - p.os = newWindowsOSImpl(p.IncludeLocalhost) + p.os = newWindowsImpl(p.IncludeLocalhost) } -type windowsOSImpl struct { +type windowsImpl struct { + known map[famPort]*portMeta includeLocalhost bool } -func newWindowsOSImpl(includeLocalhost bool) osImpl { - return &windowsOSImpl{ +type famPort struct { + proto string + port uint16 + pid uint32 +} + +type portMeta struct { + port Port + keep bool +} + +func newWindowsImpl(includeLocalhost bool) osImpl { + return &windowsImpl{ + known: map[famPort]*portMeta{}, includeLocalhost: includeLocalhost, } } +func (*windowsImpl) Close() error { return nil } -func (im *windowsOSImpl) AppendListeningPorts(_ []Port) ([]Port, error) { - return nil, ErrNotImplemented -} +func (im *windowsImpl) AppendListeningPorts(base []Port) ([]Port, error) { + // TODO(bradfitz): netstat.Get makes a bunch of garbage, add an append-style API to that package instead/additionally + // tab, err :=netstat.Get() + tab, err := GetConnTable() + if err != nil { + return nil, err + } -func (*windowsOSImpl) Close() error { return ErrNotImplemented } + for _, pm := range im.known { + pm.keep = false + } + + ret := base + for _, e := range tab.Entries { + if e.State != "LISTEN" { + continue + } + if !im.includeLocalhost && !e.Local.Addr().IsUnspecified() { + continue + } + fp := famPort{ + proto: "tcp", // TODO(bradfitz): UDP too; add to netstat + port: e.Local.Port(), + pid: uint32(e.Pid), + } + pm, ok := im.known[fp] + if ok { + pm.keep = true + continue + } + var process string + if e.OSMetadata != nil { + if module, err := e.OSMetadata.GetModule(); err == nil { + process = module + } + } + pm = &portMeta{ + keep: true, + port: Port{ + Proto: "tcp", + Port: e.Local.Port(), + Process: process, + Pid: e.Pid, + }, + } + im.known[fp] = pm + } + + for k, m := range im.known { + if !m.keep { + delete(im.known, k) + continue + } + ret = append(ret, m.port) + } + + return sortAndDedup(ret), nil +} diff --git a/test/new-e2e/tests/agent-subcommands/diagnose/diagnose_common_test.go b/test/new-e2e/tests/agent-subcommands/diagnose/diagnose_common_test.go index a2b6f52366b08..9b13aecc865ca 100644 --- a/test/new-e2e/tests/agent-subcommands/diagnose/diagnose_common_test.go +++ b/test/new-e2e/tests/agent-subcommands/diagnose/diagnose_common_test.go @@ -46,6 +46,7 @@ var commonSuites = []string{ "connectivity-datadog-autodiscovery", "connectivity-datadog-core-endpoints", "connectivity-datadog-event-platform", + "port-conflict", } func getDiagnoseOutput(v *baseDiagnoseSuite, commandArgs ...agentclient.AgentArgsOption) string {