mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-21 10:27:30 +00:00
portlist: add macOS osImpl, finish migration to new style
Previously: *036f70b7b4
for linux *35bee36549
for windows This does macOS. And removes all the compat code for the old style. (e.g. iOS, js are no longer mentioned; all platforms without implementations just default to not doing anything) One possible regression is that platforms without explicit implementations previously tried to do the "netstat -na" style to get open ports (but not process names). Maybe that worked on FreeBSD and OpenBSD previously, but nobody ever really tested it. And it was kinda useless without associated process names. So better off removing those for now until they get a good implementation. Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
da8def8e13
commit
21ef7e5c35
@@ -15,16 +15,115 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
)
|
||||
|
||||
// We have to run netstat, which is a bit expensive, so don't do it too often.
|
||||
const pollInterval = 5 * time.Second
|
||||
func init() {
|
||||
newOSImpl = newMacOSImpl
|
||||
|
||||
func appendListeningPorts(base []Port) ([]Port, error) {
|
||||
return appendListeningPortsNetstat(base, "-na")
|
||||
// We have to run netstat, which is a bit expensive, so don't do it too often.
|
||||
pollInterval = 5 * time.Second
|
||||
}
|
||||
|
||||
var lsofFailed int64 // atomic bool
|
||||
type macOSImpl struct {
|
||||
known map[protoPort]*portMeta // inode string => metadata
|
||||
netstatPath string // lazily populated
|
||||
|
||||
br *bufio.Reader // reused
|
||||
portsBuf []Port
|
||||
}
|
||||
|
||||
type protoPort struct {
|
||||
proto string
|
||||
port uint16
|
||||
}
|
||||
|
||||
type portMeta struct {
|
||||
port Port
|
||||
keep bool
|
||||
}
|
||||
|
||||
func newMacOSImpl() osImpl {
|
||||
return &macOSImpl{
|
||||
known: map[protoPort]*portMeta{},
|
||||
br: bufio.NewReader(bytes.NewReader(nil)),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -34,75 +133,82 @@ var lsofFailed int64 // atomic bool
|
||||
// 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 addProcesses(pl []Port) ([]Port, error) {
|
||||
if atomic.LoadInt64(&lsofFailed) != 0 {
|
||||
func (im *macOSImpl) addProcesses() error {
|
||||
if lsofFailed.Load() {
|
||||
// This previously failed in the macOS sandbox, so don't try again.
|
||||
return pl, nil
|
||||
return nil
|
||||
}
|
||||
exe, err := exec.LookPath("lsof")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("lsof: lookup: %v", err)
|
||||
return fmt.Errorf("lsof: lookup: %v", err)
|
||||
}
|
||||
output, err := exec.Command(exe, "-F", "-n", "-P", "-O", "-S2", "-T", "-i4", "-i6").Output()
|
||||
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 atomic.CompareAndSwapInt64(&lsofFailed, 0, 1) {
|
||||
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 pl, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
type ProtoPort struct {
|
||||
proto string
|
||||
port uint16
|
||||
}
|
||||
m := map[ProtoPort]*Port{}
|
||||
for i := range pl {
|
||||
pp := ProtoPort{pl[i].Proto, pl[i].Port}
|
||||
m[pp] = &pl[i]
|
||||
}
|
||||
|
||||
r := bytes.NewReader(output)
|
||||
scanner := bufio.NewScanner(r)
|
||||
im.br.Reset(outPipe)
|
||||
|
||||
var cmd, proto string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
for {
|
||||
line, err := im.br.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if len(line) < 1 {
|
||||
continue
|
||||
}
|
||||
field, val := line[0], line[1:]
|
||||
field, val := line[0], bytes.TrimSpace(line[1:])
|
||||
switch field {
|
||||
case 'p':
|
||||
// starting a new process
|
||||
cmd = ""
|
||||
proto = ""
|
||||
case 'c':
|
||||
cmd = val
|
||||
cmd = string(val) // TODO(bradfitz): avoid garbage; cache process names between runs?
|
||||
case 'P':
|
||||
proto = strings.ToLower(val)
|
||||
proto = lsofProtoLower(val)
|
||||
case 'n':
|
||||
if strings.Contains(val, "->") {
|
||||
if mem.Contains(mem.B(val), mem.S("->")) {
|
||||
continue
|
||||
}
|
||||
// a listening port
|
||||
port := parsePort(val)
|
||||
if port > 0 {
|
||||
pp := ProtoPort{proto, uint16(port)}
|
||||
p := m[pp]
|
||||
switch {
|
||||
case p != nil:
|
||||
p.Process = cmd
|
||||
default:
|
||||
// ignore: processes and ports come and go
|
||||
}
|
||||
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
|
||||
default:
|
||||
// ignore: processes and ports come and go
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pl, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func lsofProtoLower(p []byte) string {
|
||||
if string(p) == "TCP" {
|
||||
return "tcp"
|
||||
}
|
||||
if string(p) == "UDP" {
|
||||
return "udp"
|
||||
}
|
||||
return strings.ToLower(string(p))
|
||||
}
|
||||
|
Reference in New Issue
Block a user