mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
3697609aaa
name old time/op new time/op delta GetList-8 11.2ms ± 5% 11.1ms ± 3% ~ (p=0.661 n=10+9) name old alloc/op new alloc/op delta GetList-8 83.3kB ± 1% 67.4kB ± 1% -19.05% (p=0.000 n=10+10) name old allocs/op new allocs/op delta GetList-8 2.89k ± 2% 2.19k ± 1% -24.24% (p=0.000 n=10+10) (real issue is we're calling this code as much as we are, but easy enough to make it efficient because it'll still need to be called sometimes in any case) Updates #5958 Change-Id: I90c20278d73e80315a840aed1397d24faa308d93 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
363 lines
8.5 KiB
Go
363 lines
8.5 KiB
Go
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package portlist
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"go4.org/mem"
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
// Reading the sockfiles on Linux is very fast, so we can do it often.
|
|
const pollInterval = 1 * time.Second
|
|
|
|
var sockfiles = []string{"/proc/net/tcp", "/proc/net/tcp6", "/proc/net/udp", "/proc/net/udp6"}
|
|
|
|
var sawProcNetPermissionErr atomic.Bool
|
|
|
|
const (
|
|
v6Localhost = "00000000000000000000000001000000:"
|
|
v6Any = "00000000000000000000000000000000:0000"
|
|
v4Localhost = "0100007F:"
|
|
v4Any = "00000000:0000"
|
|
)
|
|
|
|
var eofReader = bytes.NewReader(nil)
|
|
|
|
var bufioReaderPool = &sync.Pool{
|
|
New: func() any { return bufio.NewReader(eofReader) },
|
|
}
|
|
|
|
type internedStrings struct {
|
|
m map[string]string
|
|
}
|
|
|
|
func (v *internedStrings) get(b []byte) string {
|
|
if s, ok := v.m[string(b)]; ok {
|
|
return s
|
|
}
|
|
s := string(b)
|
|
mak.Set(&v.m, s, s)
|
|
return s
|
|
}
|
|
|
|
var internedStringsPool = &sync.Pool{
|
|
New: func() any { return new(internedStrings) },
|
|
}
|
|
|
|
func appendListeningPorts(base []Port) ([]Port, error) {
|
|
ret := base
|
|
if sawProcNetPermissionErr.Load() {
|
|
return ret, nil
|
|
}
|
|
|
|
br := bufioReaderPool.Get().(*bufio.Reader)
|
|
defer bufioReaderPool.Put(br)
|
|
defer br.Reset(eofReader)
|
|
|
|
stringCache := internedStringsPool.Get().(*internedStrings)
|
|
defer internedStringsPool.Put(stringCache)
|
|
|
|
for _, fname := range sockfiles {
|
|
// 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.
|
|
if runtime.GOOS == "android" && syscall.Access(fname, unix.R_OK) != nil {
|
|
sawProcNetPermissionErr.Store(true)
|
|
return nil, nil
|
|
}
|
|
|
|
f, err := os.Open(fname)
|
|
if os.IsPermission(err) {
|
|
sawProcNetPermissionErr.Store(true)
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %s", fname, err)
|
|
}
|
|
br.Reset(f)
|
|
ret, err = appendParsePorts(ret, stringCache, br, filepath.Base(fname))
|
|
f.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing %q: %w", fname, err)
|
|
}
|
|
}
|
|
if len(stringCache.m) >= len(ret)*2 {
|
|
// Prevent unbounded growth of the internedStrings map.
|
|
stringCache.m = nil
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// fileBase is one of "tcp", "tcp6", "udp", "udp6".
|
|
func appendParsePorts(base []Port, stringCache *internedStrings, r *bufio.Reader, fileBase string) ([]Port, error) {
|
|
proto := strings.TrimSuffix(fileBase, "6")
|
|
ret := base
|
|
|
|
// skip header row
|
|
_, err := r.ReadSlice('\n')
|
|
if err != nil {
|
|
return nil, 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 err == nil {
|
|
line, err := r.ReadSlice('\n')
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, 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 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 nil, 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 nil, fmt.Errorf("%#v: %s", local.SliceFrom(9).StringCopy(), err)
|
|
}
|
|
inoBuf = append(inoBuf[:0], "socket:["...)
|
|
inoBuf = mem.Append(inoBuf, inode)
|
|
inoBuf = append(inoBuf, ']')
|
|
ret = append(ret, Port{
|
|
Proto: proto,
|
|
Port: uint16(portv),
|
|
inode: stringCache.get(inoBuf),
|
|
})
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func addProcesses(pl []Port) ([]Port, error) {
|
|
pm := map[string]*Port{} // by Port.inode
|
|
for i := range pl {
|
|
pm[pl[i].inode] = &pl[i]
|
|
}
|
|
|
|
var pathBuf []byte
|
|
|
|
err := foreachPID(func(pid string) error {
|
|
fdPath := fmt.Sprintf("/proc/%s/fd", pid)
|
|
|
|
// 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(fdPath, unix.R_OK) != nil {
|
|
return nil
|
|
}
|
|
|
|
fdDir, err := os.Open(fdPath)
|
|
if err != nil {
|
|
// Can't open fd list for this pid. Maybe
|
|
// don't have access. Ignore it.
|
|
return nil
|
|
}
|
|
defer fdDir.Close()
|
|
|
|
targetBuf := make([]byte, 64) // plenty big for "socket:[165614651]"
|
|
for {
|
|
fds, err := fdDir.Readdirnames(100)
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
// This can happen if the directory we're
|
|
// reading disappears during the run. No big
|
|
// deal.
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("addProcesses.readDir: %w", err)
|
|
}
|
|
for _, fd := range fds {
|
|
pathBuf = fmt.Appendf(pathBuf[:0], "/proc/%s/fd/%s\x00", pid, fd)
|
|
n, ok := readlink(pathBuf, targetBuf)
|
|
if !ok {
|
|
// Not a symlink or no permission.
|
|
// Skip it.
|
|
continue
|
|
}
|
|
|
|
pe := pm[string(targetBuf[:n])] // m[string([]byte)] avoids alloc
|
|
if pe != nil {
|
|
bs, err := os.ReadFile(fmt.Sprintf("/proc/%s/cmdline", pid))
|
|
if err != nil {
|
|
// Usually shouldn't happen. One possibility is
|
|
// the process has gone away, so let's skip it.
|
|
continue
|
|
}
|
|
|
|
argv := strings.Split(strings.TrimSuffix(string(bs), "\x00"), "\x00")
|
|
pe.Process = argvSubject(argv...)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pl, nil
|
|
}
|
|
|
|
func foreachPID(fn func(pidStr string) error) error {
|
|
pdir, err := os.Open("/proc")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer pdir.Close()
|
|
|
|
for {
|
|
pids, err := pdir.Readdirnames(100)
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
// This can happen if the directory we're
|
|
// reading disappears during the run. No big
|
|
// deal.
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("foreachPID.readdir: %w", err)
|
|
}
|
|
|
|
for _, pid := range pids {
|
|
_, err := strconv.ParseInt(pid, 10, 64)
|
|
if err != nil {
|
|
// not a pid, ignore it.
|
|
// /proc has lots of non-pid stuff in it.
|
|
continue
|
|
}
|
|
if err := fn(pid); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|