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>
231 lines
4.9 KiB
Go
231 lines
4.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build darwin && !ios
|
|
|
|
package portlist
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"go4.org/mem"
|
|
)
|
|
|
|
func init() {
|
|
newOSImpl = newMacOSImpl
|
|
|
|
// We have to run netstat, which is a bit expensive, so don't do it too often.
|
|
pollInterval = 5 * time.Second
|
|
}
|
|
|
|
type macOSImpl struct {
|
|
known map[protoPort]*portMeta // inode string => metadata
|
|
netstatPath string // lazily populated
|
|
|
|
br *bufio.Reader // reused
|
|
portsBuf []Port
|
|
includeLocalhost bool
|
|
}
|
|
|
|
type protoPort struct {
|
|
proto string
|
|
port uint16
|
|
}
|
|
|
|
type portMeta struct {
|
|
port Port
|
|
keep bool
|
|
}
|
|
|
|
func newMacOSImpl(includeLocalhost bool) osImpl {
|
|
return &macOSImpl{
|
|
known: map[protoPort]*portMeta{},
|
|
br: bufio.NewReader(bytes.NewReader(nil)),
|
|
includeLocalhost: includeLocalhost,
|
|
}
|
|
}
|
|
|
|
func (*macOSImpl) Close() error { return nil }
|
|
|
|
func (im *macOSImpl) AppendListeningPorts(base []Port) ([]Port, error) {
|
|
var err error
|
|
im.portsBuf, err = im.appendListeningPortsNetstat(im.portsBuf[:0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, pm := range im.known {
|
|
pm.keep = false
|
|
}
|
|
|
|
var needProcs bool
|
|
for _, p := range im.portsBuf {
|
|
fp := protoPort{
|
|
proto: p.Proto,
|
|
port: p.Port,
|
|
}
|
|
if pm, ok := im.known[fp]; ok {
|
|
pm.keep = true
|
|
} else {
|
|
needProcs = true
|
|
im.known[fp] = &portMeta{
|
|
port: p,
|
|
keep: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
ret := base
|
|
for k, m := range im.known {
|
|
if !m.keep {
|
|
delete(im.known, k)
|
|
}
|
|
}
|
|
|
|
if needProcs {
|
|
im.addProcesses() // best effort
|
|
}
|
|
|
|
for _, m := range im.known {
|
|
ret = append(ret, m.port)
|
|
}
|
|
return sortAndDedup(ret), nil
|
|
}
|
|
|
|
func (im *macOSImpl) appendListeningPortsNetstat(base []Port) ([]Port, error) {
|
|
if im.netstatPath == "" {
|
|
var err error
|
|
im.netstatPath, err = exec.LookPath("netstat")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("netstat: lookup: %v", err)
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command(im.netstatPath, "-na")
|
|
outPipe, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
im.br.Reset(outPipe)
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, err
|
|
}
|
|
defer cmd.Process.Wait()
|
|
defer cmd.Process.Kill()
|
|
|
|
return appendParsePortsNetstat(base, im.br, im.includeLocalhost)
|
|
}
|
|
|
|
var lsofFailed atomic.Bool
|
|
|
|
// In theory, lsof could replace the function of both listPorts() and
|
|
// addProcesses(), since it provides a superset of the netstat output.
|
|
// However, "netstat -na" runs ~100x faster than lsof on my machine, so
|
|
// we should do it only if the list of open ports has actually changed.
|
|
//
|
|
// This fails in a macOS sandbox (i.e. in the Mac App Store or System
|
|
// Extension GUI build), but does at least work in the
|
|
// tailscaled-on-macos mode.
|
|
func (im *macOSImpl) addProcesses() error {
|
|
if lsofFailed.Load() {
|
|
// This previously failed in the macOS sandbox, so don't try again.
|
|
return nil
|
|
}
|
|
exe, err := exec.LookPath("lsof")
|
|
if err != nil {
|
|
return fmt.Errorf("lsof: lookup: %v", err)
|
|
}
|
|
lsofCmd := exec.Command(exe, "-F", "-n", "-P", "-O", "-S2", "-T", "-i4", "-i6")
|
|
outPipe, err := lsofCmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = lsofCmd.Start()
|
|
if err != nil {
|
|
var stderr []byte
|
|
if xe, ok := err.(*exec.ExitError); ok {
|
|
stderr = xe.Stderr
|
|
}
|
|
// fails when run in a macOS sandbox, so make this non-fatal.
|
|
if lsofFailed.CompareAndSwap(false, true) {
|
|
log.Printf("portlist: can't run lsof in Mac sandbox; omitting process names from service list. Error details: %v, %s", err, bytes.TrimSpace(stderr))
|
|
}
|
|
return nil
|
|
}
|
|
defer func() {
|
|
ps, err := lsofCmd.Process.Wait()
|
|
if err != nil || ps.ExitCode() != 0 {
|
|
log.Printf("portlist: can't run lsof in Mac sandbox; omitting process names from service list. Error: %v, exit code %d", err, ps.ExitCode())
|
|
lsofFailed.Store(true)
|
|
}
|
|
}()
|
|
defer lsofCmd.Process.Kill()
|
|
|
|
im.br.Reset(outPipe)
|
|
|
|
var cmd, proto string
|
|
var pid int
|
|
for {
|
|
line, err := im.br.ReadBytes('\n')
|
|
if err != nil {
|
|
break
|
|
}
|
|
if len(line) < 1 {
|
|
continue
|
|
}
|
|
field, val := line[0], bytes.TrimSpace(line[1:])
|
|
switch field {
|
|
case 'p':
|
|
// starting a new process
|
|
cmd = ""
|
|
proto = ""
|
|
pid = 0
|
|
if p, err := mem.ParseInt(mem.B(val), 10, 0); err == nil {
|
|
pid = int(p)
|
|
}
|
|
case 'c':
|
|
cmd = string(val) // TODO(bradfitz): avoid garbage; cache process names between runs?
|
|
case 'P':
|
|
proto = lsofProtoLower(val)
|
|
case 'n':
|
|
if mem.Contains(mem.B(val), mem.S("->")) {
|
|
continue
|
|
}
|
|
// a listening port
|
|
port := parsePort(mem.B(val))
|
|
if port <= 0 {
|
|
continue
|
|
}
|
|
pp := protoPort{proto, uint16(port)}
|
|
m := im.known[pp]
|
|
switch {
|
|
case m != nil:
|
|
m.port.Process = cmd
|
|
m.port.Pid = pid
|
|
default:
|
|
// ignore: processes and ports come and go
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func lsofProtoLower(p []byte) string {
|
|
if string(p) == "TCP" {
|
|
return "tcp"
|
|
}
|
|
if string(p) == "UDP" {
|
|
return "udp"
|
|
}
|
|
return strings.ToLower(string(p))
|
|
}
|