mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			409 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			409 lines
		
	
	
		
			9.6 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 {
 | |
| 		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
 | |
| }
 |