mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-23 03:37:19 +09:00
Query listening TCP and unconnected UDP sockets through inet_diag instead of scanning the proc socket tables. Preserve process attribution and fall back to proc on unsupported, interrupted, or invalid diagnostic dumps. Cache missing protocol handlers as unsupported to avoid retrying every poll. Read terminal dump flags directly: the pinned netlink library removes NLMSG_DONE without checking NLM_F_DUMP_INTR. Keep socket creation and recovery consistent with the proc namespace and close the poller when its service discovery loop exits. Include parser, request encoding, and errno classification tests. Updates #10430 Updates #15492 Signed-off-by: JeremiahM37 <JeremiahM37@users.noreply.github.com>
801 lines
23 KiB
Go
801 lines
23 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package portlist
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"go4.org/mem"
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/util/dirwalk"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
func init() {
|
|
newOSImpl = newLinuxImpl
|
|
// Refresh service discovery once per second.
|
|
pollInterval = 1 * time.Second
|
|
}
|
|
|
|
type linuxImpl struct {
|
|
procNetFiles []*os.File // seeked to start & reused between calls
|
|
readlinkPathBuf []byte
|
|
diagBuf []byte // reused for every inet_diag receive
|
|
diagPorts map[string]Port // keyed by "socket:[<decimal inode>]", matching /proc/<pid>/fd symlink targets
|
|
|
|
known map[string]*portMeta // inode string => metadata
|
|
br *bufio.Reader
|
|
includeLocalhost bool
|
|
diagFD int // -1 means not yet opened or closed
|
|
diagPID uint32
|
|
diagSeq uint32
|
|
namespace netNamespace
|
|
diagPermanent bool
|
|
}
|
|
|
|
type portMeta struct {
|
|
port Port
|
|
pid int
|
|
keep bool
|
|
needsProcName bool
|
|
}
|
|
|
|
func newLinuxImplBase(includeLocalhost bool) *linuxImpl {
|
|
return &linuxImpl{
|
|
br: bufio.NewReader(eofReader),
|
|
known: map[string]*portMeta{},
|
|
diagPorts: map[string]Port{},
|
|
includeLocalhost: includeLocalhost,
|
|
diagFD: -1,
|
|
}
|
|
}
|
|
|
|
func newLinuxImpl(includeLocalhost bool) osImpl {
|
|
return newLinuxImplWithDiag(includeLocalhost, true)
|
|
}
|
|
|
|
// newLinuxImplWithDiag is also used by the Linux benchmarks to construct a
|
|
// proc-only implementation without opening an unused diagnostic socket.
|
|
func newLinuxImplWithDiag(includeLocalhost, useDiag bool) *linuxImpl {
|
|
// /proc/thread-self and netlink socket creation use the calling thread's
|
|
// network namespace. Stay on one thread from the namespace check through
|
|
// socket creation so both operations use the same namespace.
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
li := newLinuxImplBase(includeLocalhost)
|
|
var procErr error
|
|
li.namespace, procErr = netNamespaceID("/proc/self/ns/net")
|
|
threadNS, threadErr := netNamespaceID("/proc/thread-self/ns/net")
|
|
for _, name := range []string{
|
|
"/proc/net/tcp",
|
|
"/proc/net/tcp6",
|
|
"/proc/net/udp",
|
|
"/proc/net/udp6",
|
|
} {
|
|
f, err := os.Open(name)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
log.Printf("portlist warning; ignoring: %v", err)
|
|
continue
|
|
}
|
|
li.procNetFiles = append(li.procNetFiles, f)
|
|
}
|
|
// /proc/net is resolved through /proc/self, which denotes the process
|
|
// leader. inet_diag is scoped to the calling thread's namespace. If those
|
|
// views differ, use the complete proc fallback rather than mixing them.
|
|
if !useDiag || runtime.GOOS == "android" || (procErr != nil || threadErr != nil || threadNS != li.namespace) {
|
|
li.diagPermanent = true
|
|
} else {
|
|
li.initDiag()
|
|
}
|
|
return li
|
|
}
|
|
|
|
func (li *linuxImpl) initDiag() {
|
|
fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_INET_DIAG)
|
|
if err != nil {
|
|
li.diagPermanent = isDiagCapabilityError(err)
|
|
return
|
|
}
|
|
if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
|
|
unix.Close(fd)
|
|
li.diagPermanent = isDiagCapabilityError(err)
|
|
return
|
|
}
|
|
sa, err := unix.Getsockname(fd)
|
|
if err != nil {
|
|
unix.Close(fd)
|
|
li.diagPermanent = isDiagCapabilityError(err)
|
|
return
|
|
}
|
|
li.diagPID = sa.(*unix.SockaddrNetlink).Pid
|
|
li.diagFD = fd
|
|
}
|
|
|
|
// netNamespace identifies the namespace pinned by the proc and netlink files.
|
|
type netNamespace struct{ dev, ino uint64 }
|
|
|
|
func netNamespaceID(path string) (netNamespace, error) {
|
|
var st unix.Stat_t
|
|
if err := unix.Stat(path, &st); err != nil {
|
|
return netNamespace{}, err
|
|
}
|
|
return netNamespace{uint64(st.Dev), uint64(st.Ino)}, nil
|
|
}
|
|
|
|
func (li *linuxImpl) sameNetNamespace() bool {
|
|
ns, err := netNamespaceID("/proc/thread-self/ns/net")
|
|
return err == nil && ns == li.namespace
|
|
}
|
|
|
|
func isDiagCapabilityError(err error) bool {
|
|
// A missing protocol diagnostic handler (for example, CONFIG_INET_UDP_DIAG
|
|
// disabled) returns ENOENT after the kernel attempts to load its module.
|
|
return errors.Is(err, unix.ENOENT) || errors.Is(err, unix.EPERM) || errors.Is(err, unix.EACCES) || errors.Is(err, unix.EAFNOSUPPORT) ||
|
|
errors.Is(err, unix.EPROTONOSUPPORT) || errors.Is(err, unix.ENOPROTOOPT) ||
|
|
errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ENOSYS)
|
|
}
|
|
|
|
func (li *linuxImpl) Close() error {
|
|
li.closeDiag()
|
|
for _, f := range li.procNetFiles {
|
|
f.Close()
|
|
}
|
|
li.procNetFiles = nil
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
v6Localhost = "00000000000000000000000001000000:"
|
|
v6Any = "00000000000000000000000000000000:0000"
|
|
v4Localhost = "0100007F:"
|
|
v4Any = "00000000:0000"
|
|
)
|
|
|
|
var eofReader = bytes.NewReader(nil)
|
|
|
|
func (li *linuxImpl) AppendListeningPorts(base []Port) ([]Port, error) {
|
|
if runtime.GOOS == "android" {
|
|
// Android 10+ doesn't allow access to this anymore.
|
|
// https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem
|
|
// Ignore it rather than have the system log about our violation.
|
|
return nil, nil
|
|
}
|
|
br := li.br
|
|
defer br.Reset(eofReader)
|
|
|
|
// Start by marking all previous known ports as gone. If this mark
|
|
// bit is still false later, we'll remove them.
|
|
for _, pm := range li.known {
|
|
pm.keep = false
|
|
}
|
|
|
|
var err error
|
|
if !li.diagPermanent {
|
|
err = li.appendDiagPorts()
|
|
}
|
|
if err != nil || li.diagPermanent {
|
|
for _, f := range li.procNetFiles {
|
|
name := f.Name()
|
|
_, err := f.Seek(0, io.SeekStart)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
br.Reset(f)
|
|
err = li.parseProcNetFile(br, filepath.Base(name))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing %q: %w", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete ports that aren't open any longer.
|
|
// And see if there are any process names we need to look for.
|
|
var needProc map[string]*portMeta
|
|
for inode, pm := range li.known {
|
|
if !pm.keep {
|
|
delete(li.known, inode)
|
|
continue
|
|
}
|
|
if pm.needsProcName {
|
|
mak.Set(&needProc, inode, pm)
|
|
}
|
|
}
|
|
err = li.findProcessNames(needProc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret := base
|
|
for _, pm := range li.known {
|
|
ret = append(ret, pm.port)
|
|
}
|
|
return sortAndDedup(ret), nil
|
|
}
|
|
|
|
// appendDiagPorts obtains the same listener set as parseProcNetFile, but asks
|
|
// the kernel to do the expensive table filtering. No state is changed until
|
|
// all four dumps have completed successfully; callers can safely fall back to
|
|
// proc without leaving a partial result in known.
|
|
func (li *linuxImpl) appendDiagPorts() error {
|
|
if li.diagPermanent {
|
|
return errors.New("inet_diag unavailable")
|
|
}
|
|
if li.diagFD < 0 {
|
|
// A replacement socket must be created in the namespace where the
|
|
// proc descriptors were opened. Keep the identity check and socket
|
|
// construction on one OS thread; an existing socket remains pinned to
|
|
// its creation namespace and needs no per-poll thread pinning.
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
if !li.sameNetNamespace() {
|
|
return errors.New("inet_diag: network namespace differs from proc view")
|
|
}
|
|
li.initDiag()
|
|
if li.diagFD < 0 {
|
|
return errors.New("inet_diag unavailable")
|
|
}
|
|
}
|
|
type query struct {
|
|
family uint8
|
|
proto uint8
|
|
}
|
|
queries := []query{{unix.AF_INET, unix.IPPROTO_TCP}, {unix.AF_INET6, unix.IPPROTO_TCP}, {unix.AF_INET, unix.IPPROTO_UDP}, {unix.AF_INET6, unix.IPPROTO_UDP}}
|
|
newPorts := li.diagPorts
|
|
for inode := range newPorts {
|
|
delete(newPorts, inode)
|
|
}
|
|
for _, q := range queries {
|
|
err := li.diagDump(q.family, q.proto, newPorts)
|
|
if err != nil {
|
|
if isDiagCapabilityError(err) {
|
|
li.diagPermanent = true
|
|
}
|
|
li.closeDiag()
|
|
return err
|
|
}
|
|
}
|
|
for inode, pm := range li.known {
|
|
if p, ok := newPorts[inode]; ok {
|
|
pm.keep = true
|
|
if pm.port.Proto != p.Proto || pm.port.Port != p.Port {
|
|
pm.port.Process, pm.port.Pid = "", 0
|
|
pm.needsProcName = true
|
|
}
|
|
pm.port.Proto, pm.port.Port = p.Proto, p.Port
|
|
continue
|
|
}
|
|
delete(li.known, inode)
|
|
}
|
|
for inode, p := range newPorts {
|
|
if _, ok := li.known[inode]; !ok {
|
|
li.known[inode] = &portMeta{port: p, keep: true, needsProcName: true}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (li *linuxImpl) closeDiag() {
|
|
if li.diagFD >= 0 {
|
|
_ = unix.Close(li.diagFD)
|
|
li.diagFD = -1
|
|
li.diagPID = 0
|
|
}
|
|
}
|
|
|
|
// tcpListen is Linux's TCP_LISTEN state from include/net/tcp_states.h.
|
|
const tcpListen = 10
|
|
|
|
func diagRequest(family, proto uint8) []byte {
|
|
// Pack these layouts from include/uapi/linux/inet_diag.h. Offsets below
|
|
// are relative to the start of each struct; states uses native byte order.
|
|
//
|
|
// struct inet_diag_sockid { // 48 bytes
|
|
// __be16 idiag_sport; // 0
|
|
// __be16 idiag_dport; // 2
|
|
// __be32 idiag_src[4]; // 4
|
|
// __be32 idiag_dst[4]; // 20
|
|
// __u32 idiag_if; // 36
|
|
// __u32 idiag_cookie[2]; // 40
|
|
// };
|
|
// struct inet_diag_req_v2 { // 56 bytes
|
|
// __u8 sdiag_family; // 0
|
|
// __u8 sdiag_protocol; // 1
|
|
// __u8 idiag_ext; // 2
|
|
// __u8 pad; // 3
|
|
// __u32 idiag_states; // 4
|
|
// struct inet_diag_sockid id; // 8
|
|
// };
|
|
b := make([]byte, 56)
|
|
b[0], b[1] = family, proto
|
|
// Query TCP_LISTEN only. On Linux, entries with a zero remote endpoint in
|
|
// /proc/net/tcp are listeners; requesting other TCP states is substantially
|
|
// slower and does not add ports. UDP uses all states and retains the exact
|
|
// zero-remote filtering above.
|
|
if proto == unix.IPPROTO_TCP {
|
|
binary.NativeEndian.PutUint32(b[4:8], 1<<tcpListen)
|
|
} else {
|
|
binary.NativeEndian.PutUint32(b[4:8], ^uint32(0))
|
|
}
|
|
return b
|
|
}
|
|
|
|
const diagTimeout = 250 * time.Millisecond
|
|
|
|
// diagDump performs one complete multipart SOCK_DIAG_BY_FAMILY transaction.
|
|
// The pinned mdlayher/netlink Conn.Receive (also used by Execute) removes
|
|
// NLMSG_DONE without checking its NLM_F_DUMP_INTR flag. Reading datagrams
|
|
// directly lets us reject interruptions reported only on that final message.
|
|
func (li *linuxImpl) diagDump(family, proto uint8, ports map[string]Port) error {
|
|
li.diagSeq++
|
|
if li.diagSeq == 0 {
|
|
li.diagSeq++
|
|
}
|
|
seq := li.diagSeq
|
|
data := diagRequest(family, proto)
|
|
msg := make([]byte, unix.NLMSG_HDRLEN+len(data))
|
|
binary.NativeEndian.PutUint32(msg[0:4], uint32(len(msg)))
|
|
binary.NativeEndian.PutUint16(msg[4:6], unix.SOCK_DIAG_BY_FAMILY)
|
|
binary.NativeEndian.PutUint16(msg[6:8], unix.NLM_F_REQUEST|unix.NLM_F_DUMP)
|
|
binary.NativeEndian.PutUint32(msg[8:12], seq)
|
|
copy(msg[unix.NLMSG_HDRLEN:], data)
|
|
deadline := time.Now().Add(diagTimeout)
|
|
if err := unix.Sendto(li.diagFD, msg, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil {
|
|
return err
|
|
}
|
|
if li.diagBuf == nil {
|
|
li.diagBuf = make([]byte, 128<<10)
|
|
}
|
|
const (
|
|
maxMessages = 1 << 20
|
|
maxRecords = 1 << 20
|
|
maxBytes = 128 << 20
|
|
)
|
|
messages := 0
|
|
records := 0
|
|
var totalBytes int
|
|
for messages < maxMessages {
|
|
remaining := time.Until(deadline)
|
|
if remaining <= 0 {
|
|
return errors.New("inet_diag: receive deadline exceeded")
|
|
}
|
|
tv := unix.NsecToTimeval(remaining.Nanoseconds())
|
|
if tv.Sec == 0 && tv.Usec == 0 {
|
|
tv.Usec = 1
|
|
}
|
|
if err := unix.SetsockoptTimeval(li.diagFD, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv); err != nil {
|
|
return err
|
|
}
|
|
n, _, flags, from, err := unix.Recvmsg(li.diagFD, li.diagBuf, nil, 0)
|
|
if err == unix.EINTR {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n > maxBytes-totalBytes {
|
|
return errors.New("inet_diag: response byte limit exceeded")
|
|
}
|
|
totalBytes += n
|
|
if flags&unix.MSG_TRUNC != 0 {
|
|
return errors.New("inet_diag: truncated netlink datagram")
|
|
}
|
|
if sa, ok := from.(*unix.SockaddrNetlink); !ok || sa.Pid != 0 {
|
|
return errors.New("inet_diag: unexpected netlink sender")
|
|
}
|
|
done, err := parseDiagDatagram(li.diagBuf[:n], seq, li.diagPID, family, func(body []byte) error {
|
|
records++
|
|
if records > maxRecords {
|
|
return errors.New("inet_diag: response record limit exceeded")
|
|
}
|
|
// inet_diag_msg has the same address layout as the request. The
|
|
// remote address begins at 24 and inode is the final uint32.
|
|
remote := body[24:40]
|
|
if !allZero(remote) || body[6] != 0 || body[7] != 0 {
|
|
return nil
|
|
}
|
|
local := body[8:24]
|
|
if !li.includeLocalhost && (isDiagV4Localhost(local, family) || isDiagV6Localhost(local, family)) {
|
|
return nil
|
|
}
|
|
port := uint16(body[4])<<8 | uint16(body[5])
|
|
inode := uint64(binary.NativeEndian.Uint32(body[68:72]))
|
|
key := fmt.Sprintf("socket:[%d]", inode)
|
|
ports[key] = Port{Proto: protocolName(proto), Port: port}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
messages++
|
|
if done {
|
|
return nil
|
|
}
|
|
}
|
|
return errors.New("inet_diag: response limit exceeded")
|
|
}
|
|
|
|
func protocolName(proto uint8) string {
|
|
if proto == unix.IPPROTO_TCP {
|
|
return "tcp"
|
|
}
|
|
return "udp"
|
|
}
|
|
|
|
// parseDiagDatagram validates one received netlink datagram for an
|
|
// inet_diag transaction and invokes onRecord for each diagnostic record.
|
|
// It is pure with respect to transport state: callback body slices refer to
|
|
// datagram and must be consumed or copied before returning. A false result
|
|
// means the multipart transaction has not received NLMSG_DONE yet; the caller
|
|
// must continue receiving and reject timeout or EOF without a true result.
|
|
func parseDiagDatagram(datagram []byte, seq, pid uint32, family uint8, onRecord func([]byte) error) (done bool, err error) {
|
|
for off := 0; off < len(datagram); {
|
|
if len(datagram)-off < unix.NLMSG_HDRLEN {
|
|
return false, errors.New("inet_diag: truncated netlink header")
|
|
}
|
|
mlen := int(binary.NativeEndian.Uint32(datagram[off : off+4]))
|
|
if mlen < unix.NLMSG_HDRLEN || mlen > len(datagram)-off {
|
|
return false, errors.New("inet_diag: malformed netlink length")
|
|
}
|
|
aligned := (mlen + 3) &^ 3
|
|
if aligned > len(datagram)-off {
|
|
return false, errors.New("inet_diag: missing netlink alignment padding")
|
|
}
|
|
for _, p := range datagram[off+mlen : off+aligned] {
|
|
if p != 0 {
|
|
return false, errors.New("inet_diag: non-zero netlink padding")
|
|
}
|
|
}
|
|
typ := binary.NativeEndian.Uint16(datagram[off+4 : off+6])
|
|
flags := binary.NativeEndian.Uint16(datagram[off+6 : off+8])
|
|
mseq := binary.NativeEndian.Uint32(datagram[off+8 : off+12])
|
|
mpid := binary.NativeEndian.Uint32(datagram[off+12 : off+16])
|
|
if mseq != seq || mpid != pid {
|
|
return false, errors.New("inet_diag: mismatched netlink response")
|
|
}
|
|
if flags&unix.NLM_F_DUMP_INTR != 0 {
|
|
return false, errors.New("inet_diag: dump interrupted")
|
|
}
|
|
body := datagram[off+unix.NLMSG_HDRLEN : off+mlen]
|
|
switch typ {
|
|
case unix.NLMSG_DONE:
|
|
if mlen != unix.NLMSG_HDRLEN && mlen != unix.NLMSG_HDRLEN+4 {
|
|
return false, errors.New("inet_diag: malformed netlink completion")
|
|
}
|
|
if len(body) == 4 && binary.NativeEndian.Uint32(body) != 0 {
|
|
return false, diagNetlinkError(int32(binary.NativeEndian.Uint32(body)))
|
|
}
|
|
if off+aligned != len(datagram) {
|
|
return false, errors.New("inet_diag: data after netlink completion")
|
|
}
|
|
return true, nil
|
|
case unix.NLMSG_ERROR:
|
|
if len(body) < 4 {
|
|
return false, errors.New("inet_diag: truncated netlink error")
|
|
}
|
|
code := int32(binary.NativeEndian.Uint32(body[:4]))
|
|
if code != 0 {
|
|
return false, diagNetlinkError(code)
|
|
}
|
|
return false, errors.New("inet_diag: netlink ACK without completion")
|
|
default:
|
|
if typ != unix.SOCK_DIAG_BY_FAMILY || flags&unix.NLM_F_MULTI == 0 {
|
|
return false, errors.New("inet_diag: unexpected netlink response")
|
|
}
|
|
if len(body) < 72 {
|
|
return false, fmt.Errorf("inet_diag: short response (%d bytes)", len(body))
|
|
}
|
|
if body[0] != family {
|
|
return false, errors.New("inet_diag: response family mismatch")
|
|
}
|
|
if onRecord != nil {
|
|
if err := onRecord(body); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
}
|
|
off += aligned
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func diagNetlinkError(code int32) error {
|
|
if code > 0 {
|
|
return fmt.Errorf("inet_diag: malformed netlink error code %d", code)
|
|
}
|
|
return fmt.Errorf("inet_diag: netlink error: %w", unix.Errno(-code))
|
|
}
|
|
|
|
func allZero(b []byte) bool {
|
|
for _, x := range b {
|
|
if x != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
func isDiagV4Localhost(b []byte, family uint8) bool {
|
|
return family == unix.AF_INET && b[0] == 127 && b[1] == 0 && b[2] == 0 && b[3] == 1 && allZero(b[4:])
|
|
}
|
|
func isDiagV6Localhost(b []byte, family uint8) bool {
|
|
return family == unix.AF_INET6 && allZero(b[:15]) && b[15] == 1
|
|
}
|
|
|
|
// fileBase is one of "tcp", "tcp6", "udp", "udp6".
|
|
func (li *linuxImpl) parseProcNetFile(r *bufio.Reader, fileBase string) error {
|
|
proto := strings.TrimSuffix(fileBase, "6")
|
|
|
|
// skip header row
|
|
_, err := r.ReadSlice('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fields := make([]mem.RO, 0, 20) // 17 current fields + some future slop
|
|
|
|
wantRemote := mem.S(v4Any)
|
|
if strings.HasSuffix(fileBase, "6") {
|
|
wantRemote = mem.S(v6Any)
|
|
}
|
|
|
|
// remoteIndex is the index within a line to the remote address field.
|
|
// -1 means not yet found.
|
|
remoteIndex := -1
|
|
|
|
// Add an upper bound on how many rows we'll attempt to read just
|
|
// to make sure this doesn't consume too much of their CPU.
|
|
// TODO(bradfitz,crawshaw): adaptively adjust polling interval as function
|
|
// of open sockets.
|
|
const maxRows = 1e6
|
|
rows := 0
|
|
|
|
// Scratch buffer for making inode strings.
|
|
inoBuf := make([]byte, 0, 50)
|
|
|
|
for {
|
|
line, err := r.ReadSlice('\n')
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows++
|
|
if rows >= maxRows {
|
|
break
|
|
}
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
// On the first row of output, find the index of the 3rd field (index 2),
|
|
// the remote address. All the rows are aligned, at least until 4 billion open
|
|
// TCP connections, per the Linux get_tcp4_sock's "%4d: " on an int i.
|
|
if remoteIndex == -1 {
|
|
remoteIndex = fieldIndex(line, 2)
|
|
if remoteIndex == -1 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(line) < remoteIndex || !mem.HasPrefix(mem.B(line).SliceFrom(remoteIndex), wantRemote) {
|
|
// Fast path for not being a listener port.
|
|
continue
|
|
}
|
|
|
|
// sl local rem ... inode
|
|
fields = mem.AppendFields(fields[:0], mem.B(line))
|
|
local := fields[1]
|
|
rem := fields[2]
|
|
inode := fields[9]
|
|
|
|
if !rem.Equal(wantRemote) {
|
|
// not a "listener" port
|
|
continue
|
|
}
|
|
|
|
// If a port is bound to localhost, ignore it.
|
|
// TODO: localhost is bigger than 1 IP, we need to ignore
|
|
// more things.
|
|
if !li.includeLocalhost && (mem.HasPrefix(local, mem.S(v4Localhost)) || mem.HasPrefix(local, mem.S(v6Localhost))) {
|
|
continue
|
|
}
|
|
|
|
// Don't use strings.Split here, because it causes
|
|
// allocations significant enough to show up in profiles.
|
|
i := mem.IndexByte(local, ':')
|
|
if i == -1 {
|
|
return fmt.Errorf("%q unexpectedly didn't have a colon", local.StringCopy())
|
|
}
|
|
portv, err := mem.ParseUint(local.SliceFrom(i+1), 16, 16)
|
|
if err != nil {
|
|
return fmt.Errorf("%#v: %s", local.SliceFrom(9).StringCopy(), err)
|
|
}
|
|
inoBuf = append(inoBuf[:0], "socket:["...)
|
|
inoBuf = mem.Append(inoBuf, inode)
|
|
inoBuf = append(inoBuf, ']')
|
|
|
|
if pm, ok := li.known[string(inoBuf)]; ok {
|
|
pm.keep = true
|
|
// Rest should be unchanged.
|
|
} else {
|
|
li.known[string(inoBuf)] = &portMeta{
|
|
needsProcName: true,
|
|
keep: true,
|
|
port: Port{
|
|
Proto: proto,
|
|
Port: uint16(portv),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// errDone is an internal sentinel error that we found everything we were looking for.
|
|
var errDone = errors.New("done")
|
|
|
|
// need is keyed by inode string.
|
|
func (li *linuxImpl) findProcessNames(need map[string]*portMeta) error {
|
|
if len(need) == 0 {
|
|
return nil
|
|
}
|
|
defer func() {
|
|
// Anything we didn't find, give up on and don't try to look for it later.
|
|
for _, pm := range need {
|
|
pm.needsProcName = false
|
|
}
|
|
}()
|
|
|
|
err := foreachPID(func(pid mem.RO) error {
|
|
var procBuf [128]byte
|
|
fdPath := mem.Append(procBuf[:0], mem.S("/proc/"))
|
|
fdPath = mem.Append(fdPath, pid)
|
|
fdPath = mem.Append(fdPath, mem.S("/fd"))
|
|
|
|
// Android logs a bunch of audit violations in logcat
|
|
// if we try to open things we don't have access
|
|
// to. So on Android only, ask if we have permission
|
|
// rather than just trying it to determine whether we
|
|
// have permission.
|
|
if runtime.GOOS == "android" && syscall.Access(string(fdPath), unix.R_OK) != nil {
|
|
return nil
|
|
}
|
|
|
|
dirwalk.WalkShallow(mem.B(fdPath), func(fd mem.RO, de fs.DirEntry) error {
|
|
targetBuf := make([]byte, 64) // plenty big for "socket:[165614651]"
|
|
|
|
linkPath := li.readlinkPathBuf[:0]
|
|
linkPath = fmt.Appendf(linkPath, "/proc/")
|
|
linkPath = mem.Append(linkPath, pid)
|
|
linkPath = append(linkPath, "/fd/"...)
|
|
linkPath = mem.Append(linkPath, fd)
|
|
linkPath = append(linkPath, 0) // terminating NUL
|
|
li.readlinkPathBuf = linkPath // to reuse its buffer next time
|
|
n, ok := readlink(linkPath, targetBuf)
|
|
if !ok {
|
|
// Not a symlink or no permission.
|
|
// Skip it.
|
|
return nil
|
|
}
|
|
|
|
pe := need[string(targetBuf[:n])] // m[string([]byte)] avoids alloc
|
|
if pe == nil {
|
|
return nil
|
|
}
|
|
bs, err := os.ReadFile(fmt.Sprintf("/proc/%s/cmdline", pid.StringCopy()))
|
|
if err != nil {
|
|
// Usually shouldn't happen. One possibility is
|
|
// the process has gone away, so let's skip it.
|
|
return nil
|
|
}
|
|
|
|
argv := strings.Split(strings.TrimSuffix(string(bs), "\x00"), "\x00")
|
|
if p, err := mem.ParseInt(pid, 10, 0); err == nil {
|
|
pe.pid = int(p)
|
|
}
|
|
pe.port.Process = argvSubject(argv...)
|
|
pid64, _ := mem.ParseInt(pid, 10, 0)
|
|
pe.port.Pid = int(pid64)
|
|
pe.needsProcName = false
|
|
delete(need, string(targetBuf[:n]))
|
|
if len(need) == 0 {
|
|
return errDone
|
|
}
|
|
return nil
|
|
})
|
|
return nil
|
|
})
|
|
if err == errDone {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func foreachPID(fn func(pidStr mem.RO) error) error {
|
|
err := dirwalk.WalkShallow(mem.S("/proc"), func(name mem.RO, de fs.DirEntry) error {
|
|
if !isNumeric(name) {
|
|
return nil
|
|
}
|
|
return fn(name)
|
|
})
|
|
if os.IsNotExist(err) {
|
|
// This can happen if the directory we're
|
|
// reading disappears during the run. No big
|
|
// deal.
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func isNumeric(s mem.RO) bool {
|
|
for i, n := 0, s.Len(); i < n; i++ {
|
|
b := s.At(i)
|
|
if b < '0' || b > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return s.Len() > 0
|
|
}
|
|
|
|
// fieldIndex returns the offset in line where the Nth field (0-based) begins, or -1
|
|
// if there aren't that many fields. Fields are separated by 1 or more spaces.
|
|
func fieldIndex(line []byte, n int) int {
|
|
skip := 0
|
|
for i := 0; i <= n; i++ {
|
|
// Skip spaces.
|
|
for skip < len(line) && line[skip] == ' ' {
|
|
skip++
|
|
}
|
|
if skip == len(line) {
|
|
return -1
|
|
}
|
|
if i == n {
|
|
break
|
|
}
|
|
// Skip non-space.
|
|
for skip < len(line) && line[skip] != ' ' {
|
|
skip++
|
|
}
|
|
}
|
|
return skip
|
|
}
|
|
|
|
// path must be null terminated.
|
|
func readlink(path, buf []byte) (n int, ok bool) {
|
|
if len(buf) == 0 || len(path) < 2 || path[len(path)-1] != 0 {
|
|
return 0, false
|
|
}
|
|
var dirfd int = unix.AT_FDCWD
|
|
r0, _, e1 := unix.Syscall6(unix.SYS_READLINKAT,
|
|
uintptr(dirfd),
|
|
uintptr(unsafe.Pointer(&path[0])),
|
|
uintptr(unsafe.Pointer(&buf[0])),
|
|
uintptr(len(buf)),
|
|
0, 0)
|
|
n = int(r0)
|
|
if e1 != 0 {
|
|
return 0, false
|
|
}
|
|
return n, true
|
|
}
|