mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
036f70b7b4
Add an osImpl interface that can be stateful and thus more efficient between calls. It will later be implemented by all OSes but for now this change only adds a Linux implementation. Remove Port.inode. It was only used by Linux and moves into its osImpl. Don't reopen /proc/net/* files on each run. Turns out you can just keep then open and seek to the beginning and reread and the contents are fresh. name old time/op new time/op delta GetListIncremental-8 7.29ms ± 2% 6.53ms ± 1% -10.50% (p=0.000 n=9+9) name old alloc/op new alloc/op delta GetListIncremental-8 1.30kB ±13% 0.70kB ± 5% -46.38% (p=0.000 n=9+10) name old allocs/op new allocs/op delta GetListIncremental-8 33.2 ±11% 18.0 ± 0% -45.82% (p=0.000 n=9+10) Updates #5958 Change-Id: I4be83463cbd23c2e2fa5d0bdf38560004f53401b Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
431 lines
9.7 KiB
Go
431 lines
9.7 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"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"go4.org/mem"
|
|
"golang.org/x/sys/unix"
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
func init() {
|
|
newOSImpl = newLinuxImpl
|
|
}
|
|
|
|
type linuxImpl struct {
|
|
procNetFiles []*os.File // seeked to start & reused between calls
|
|
|
|
known map[string]*portMeta // inode string => metadata
|
|
br *bufio.Reader
|
|
}
|
|
|
|
type portMeta struct {
|
|
port Port
|
|
keep bool
|
|
needsProcName bool
|
|
}
|
|
|
|
func newLinuxImplBase() *linuxImpl {
|
|
return &linuxImpl{
|
|
br: bufio.NewReader(eofReader),
|
|
known: map[string]*portMeta{},
|
|
}
|
|
}
|
|
|
|
func newLinuxImpl() osImpl {
|
|
li := newLinuxImplBase()
|
|
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)
|
|
}
|
|
return li
|
|
}
|
|
|
|
func (li *linuxImpl) Close() error {
|
|
for _, f := range li.procNetFiles {
|
|
f.Close()
|
|
}
|
|
li.procNetFiles = nil
|
|
return nil
|
|
}
|
|
|
|
// Reading the sockfiles on Linux is very fast, so we can do it often.
|
|
const pollInterval = 1 * time.Second
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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 err == nil {
|
|
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 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
|
|
}
|
|
}()
|
|
|
|
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 := need[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.port.Process = argvSubject(argv...)
|
|
pe.needsProcName = false
|
|
delete(need, string(targetBuf[:n]))
|
|
if len(need) == 0 {
|
|
return errDone
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
if err == errDone {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func appendListeningPorts([]Port) ([]Port, error) {
|
|
panic("unused on linux; needed to compile for now")
|
|
}
|
|
|
|
func addProcesses([]Port) ([]Port, error) {
|
|
panic("unused on linux; needed to compile for now")
|
|
}
|