mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
e32e5c0d0c
This PR parameterizes receiving loopback updates from the portlist package. Callers can now include services bound to localhost if they want. Note that this option is off by default still. Fixes #8171 Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
407 lines
9.5 KiB
Go
407 lines
9.5 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package portlist
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"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
|
|
// Reading the sockfiles on Linux is very fast, so we can do it often.
|
|
pollInterval = 1 * time.Second
|
|
}
|
|
|
|
type linuxImpl struct {
|
|
procNetFiles []*os.File // seeked to start & reused between calls
|
|
readlinkPathBuf []byte
|
|
|
|
known map[string]*portMeta // inode string => metadata
|
|
br *bufio.Reader
|
|
includeLocalhost 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{},
|
|
includeLocalhost: includeLocalhost,
|
|
}
|
|
}
|
|
|
|
func newLinuxImpl(includeLocalhost bool) osImpl {
|
|
li := newLinuxImplBase(includeLocalhost)
|
|
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
|
|
}
|
|
|
|
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 !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...)
|
|
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
|
|
}
|