| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- // 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
- }
|