tailscale/portlist/netstat.go

133 lines
3.1 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin && !ios
package portlist
import (
"bufio"
"bytes"
"io"
"go4.org/mem"
)
// parsePort returns the port number at the end of s following the last "." or
// ":", whichever comes last. It returns -1 on a parse error or invalid number
// and 0 if the port number was "*".
//
// This is basically net.SplitHostPort except that it handles a "." (as macOS
// and others return in netstat output), uses mem.RO, and validates that the
// port must be numeric and in the uint16 range.
func parsePort(s mem.RO) int {
// a.b.c.d:1234 or [a:b:c:d]:1234
i1 := mem.LastIndexByte(s, ':')
// a.b.c.d.1234 or [a:b:c:d].1234
i2 := mem.LastIndexByte(s, '.')
i := i1
if i2 > i {
i = i2
}
if i < 0 {
// no match; weird
return -1
}
portstr := s.SliceFrom(i + 1)
if portstr.EqualString("*") {
return 0
}
port, err := mem.ParseUint(portstr, 10, 16)
if err != nil {
// invalid port; weird
return -1
}
return int(port)
}
func isLoopbackAddr(s mem.RO) bool {
return mem.HasPrefix(s, mem.S("127.")) ||
mem.HasPrefix(s, mem.S("[::1]:")) ||
mem.HasPrefix(s, mem.S("::1."))
}
// appendParsePortsNetstat appends to base listening ports
// from "netstat" output, read from br. See TestParsePortsNetstat
// for example input lines.
//
// This used to be a lowest common denominator parser for "netstat -na" format.
// All of Linux, Windows, and macOS support -na and give similar-ish output
// formats that we can parse without special detection logic.
// Unfortunately, options to filter by proto or state are non-portable,
// so we'll filter for ourselves.
// Nowadays, though, we only use it for macOS as of 2022-11-04.
func appendParsePortsNetstat(base []Port, br *bufio.Reader, includeLocalhost bool) ([]Port, error) {
ret := base
var fieldBuf [10]mem.RO
for {
line, err := br.ReadBytes('\n')
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
trimline := bytes.TrimSpace(line)
cols := mem.AppendFields(fieldBuf[:0], mem.B(trimline))
if len(cols) < 1 {
continue
}
protos := cols[0]
var proto string
var laddr, raddr mem.RO
if mem.HasPrefixFold(protos, mem.S("tcp")) {
if len(cols) < 4 {
continue
}
proto = "tcp"
laddr = cols[len(cols)-3]
raddr = cols[len(cols)-2]
state := cols[len(cols)-1]
if !mem.HasPrefix(state, mem.S("LISTEN")) {
// not interested in non-listener sockets
continue
}
if !includeLocalhost && isLoopbackAddr(laddr) {
// not interested in loopback-bound listeners
continue
}
} else if mem.HasPrefixFold(protos, mem.S("udp")) {
if len(cols) < 3 {
continue
}
proto = "udp"
laddr = cols[len(cols)-2]
raddr = cols[len(cols)-1]
if !includeLocalhost && isLoopbackAddr(laddr) {
// not interested in loopback-bound listeners
continue
}
} else {
// not interested in other protocols
continue
}
lport := parsePort(laddr)
rport := parsePort(raddr)
if rport > 0 || lport <= 0 {
// not interested in "connected" sockets
continue
}
ret = append(ret, Port{
Proto: proto,
Port: uint16(lport),
})
}
return ret, nil
}