Move Linux client & common packages into a public repo.

This commit is contained in:
Earl Lee
2020-02-05 14:16:58 -08:00
parent c955043dfe
commit a8d8b8719a
156 changed files with 17113 additions and 0 deletions

155
portlist/netstat.go Normal file
View File

@@ -0,0 +1,155 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"fmt"
"sort"
"strconv"
"strings"
exec "tailscale.com/tempfork/osexec"
)
func parsePort(s string) int {
// a.b.c.d:1234 or [a:b:c:d]:1234
i1 := strings.LastIndexByte(s, ':')
// a.b.c.d.1234 or [a:b:c:d].1234
i2 := strings.LastIndexByte(s, '.')
i := i1
if i2 > i {
i = i2
}
if i < 0 {
// no match; weird
return -1
}
portstr := s[i+1 : len(s)]
if portstr == "*" {
return 0
}
port, err := strconv.ParseUint(portstr, 10, 16)
if err != nil {
// invalid port; weird
return -1
}
return int(port)
}
type nothing struct{}
// 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.
func parsePortsNetstat(output string) List {
m := map[Port]nothing{}
lines := strings.Split(string(output), "\n")
var lastline string
var lastport Port
for _, line := range lines {
trimline := strings.TrimSpace(line)
cols := strings.Fields(trimline)
if len(cols) < 1 {
continue
}
protos := strings.ToLower(cols[0])
var proto, laddr, raddr string
if strings.HasPrefix(protos, "tcp") {
if len(cols) < 4 {
continue
}
proto = "tcp"
laddr = cols[len(cols)-3]
raddr = cols[len(cols)-2]
state := cols[len(cols)-1]
if !strings.HasPrefix(state, "LISTEN") {
// not interested in non-listener sockets
continue
}
} else if strings.HasPrefix(protos, "udp") {
if len(cols) < 3 {
continue
}
proto = "udp"
laddr = cols[len(cols)-2]
raddr = cols[len(cols)-1]
} else if protos[0] == '[' && len(trimline) > 2 {
// Windows: with netstat -nab, appends a line like:
// [description]
// after the port line.
p := lastport
delete(m, lastport)
proc := trimline[1 : len(trimline)-1]
if proc == "svchost.exe" && lastline != "" {
p.Process = lastline
} else {
if strings.HasSuffix(proc, ".exe") {
p.Process = proc[:len(proc)-4]
} else {
p.Process = proc
}
}
m[p] = nothing{}
} else {
// not interested in other protocols
lastline = trimline
continue
}
lport := parsePort(laddr)
rport := parsePort(raddr)
if rport != 0 || lport <= 0 {
// not interested in "connected" sockets
continue
}
p := Port{
Proto: proto,
Port: uint16(lport),
}
m[p] = nothing{}
lastport = p
lastline = ""
}
l := []Port{}
for p := range m {
l = append(l, p)
}
sort.Slice(l, func(i, j int) bool {
return (&l[i]).lessThan(&l[j])
})
return l
}
func listPortsNetstat(args string) (List, error) {
exe, err := exec.LookPath("netstat")
if err != nil {
return nil, fmt.Errorf("netstat: lookup: %v", err)
}
c := exec.Cmd{
Path: exe,
Args: []string{exe, args},
}
output, err := c.Output()
if err != nil {
xe, ok := err.(*exec.ExitError)
stderr := ""
if ok {
stderr = strings.TrimSpace(string(xe.Stderr))
}
return nil, fmt.Errorf("netstat: %v (%q)", err, stderr)
}
return parsePortsNetstat(string(output)), nil
}

89
portlist/netstat_test.go Normal file
View File

@@ -0,0 +1,89 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"fmt"
"testing"
)
func TestParsePort(t *testing.T) {
type InOut struct {
in string
expect int
}
tests := []InOut{
InOut{"1.2.3.4:5678", 5678},
InOut{"0.0.0.0.999", 999},
InOut{"1.2.3.4:*", 0},
InOut{"5.5.5.5:0", 0},
InOut{"[1::2]:5", 5},
InOut{"[1::2].5", 5},
InOut{"gibberish", -1},
}
for _, io := range tests {
got := parsePort(io.in)
if got != io.expect {
t.Fatalf("input:%#v expect:%v got:%v\n", io.in, io.expect, got)
}
}
}
var netstat_output = `
// linux
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
udp 0 0 0.0.0.0:5353 0.0.0.0:*
udp6 0 0 :::5353 :::*
udp6 0 0 :::5354 :::*
// macOS
tcp4 0 0 *.23 *.* LISTEN
tcp6 0 0 *.24 *.* LISTEN
udp6 0 0 *.5453 *.*
udp4 0 0 *.5553 *.*
// Windows 10
Proto Local Address Foreign Address State
TCP 0.0.0.0:32 0.0.0.0:0 LISTENING
[sshd.exe]
UDP 0.0.0.0:5050 *:*
CDPSvc
[svchost.exe]
UDP 0.0.0.0:53 *:*
[chrome.exe]
UDP 10.0.1.43:9353 *:*
[iTunes.exe]
UDP [::]:53 *:*
UDP [::]:53 *:*
[funball.exe]
`
func TestParsePortsNetstat(t *testing.T) {
expect := List{
Port{"tcp", 22, "", ""},
Port{"tcp", 23, "", ""},
Port{"tcp", 24, "", ""},
Port{"tcp", 32, "", "sshd"},
Port{"udp", 53, "", "chrome"},
Port{"udp", 53, "", "funball"},
Port{"udp", 5050, "", "CDPSvc"},
Port{"udp", 5353, "", ""},
Port{"udp", 5354, "", ""},
Port{"udp", 5453, "", ""},
Port{"udp", 5553, "", ""},
Port{"udp", 9353, "", "iTunes"},
}
pl := parsePortsNetstat(netstat_output)
fmt.Printf("--- expect:\n%v\n", expect)
fmt.Printf("--- got:\n%v\n", pl)
for i := range pl {
if expect[i] != pl[i] {
t.Fatalf("row#%d\n expect=%v\n got=%v\n",
i, expect[i], pl[i])
}
}
}

59
portlist/poller.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"time"
)
type Poller struct {
C chan List // new data when it arrives; closed when done
quitCh chan struct{} // close this to force exit
Err error // last returned error code, if any
prev List // most recent data
}
func NewPoller() (*Poller, error) {
p := &Poller{
C: make(chan List),
quitCh: make(chan struct{}),
}
// Do one initial poll synchronously, so the caller can react
// to any obvious errors.
p.prev, p.Err = GetList(nil)
return p, p.Err
}
func (p *Poller) Close() {
close(p.quitCh)
<-p.C
}
// Poll periodically. Run this in a goroutine if you want.
func (p *Poller) Run() error {
defer close(p.C)
tick := time.NewTicker(POLL_SECONDS * time.Second)
defer tick.Stop()
// Send out the pre-generated initial value
p.C <- p.prev
for {
select {
case <-tick.C:
pl, err := GetList(p.prev)
if err != nil {
p.Err = err
return p.Err
}
if !pl.SameInodes(p.prev) {
p.prev = pl
p.C <- pl
}
case <-p.quitCh:
return nil
}
}
}

87
portlist/portlist.go Normal file
View File

@@ -0,0 +1,87 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"fmt"
"strings"
)
type Port struct {
Proto string
Port uint16
inode string
Process string
}
type List []Port
var protos = []string{"tcp", "udp"}
func (a *Port) lessThan(b *Port) bool {
if a.Port < b.Port {
return true
} else if a.Port > b.Port {
return false
}
if a.Proto < b.Proto {
return true
} else if a.Proto > b.Proto {
return false
}
if a.inode < b.inode {
return true
} else if a.inode > b.inode {
return false
}
if a.Process < b.Process {
return true
} else if a.Process > b.Process {
return false
}
return false
}
func (a List) SameInodes(b List) bool {
if a == nil || b == nil || len(a) != len(b) {
return false
}
for i := range a {
if a[i].Proto != b[i].Proto ||
a[i].Port != b[i].Port ||
a[i].inode != b[i].inode {
return false
}
}
return true
}
func (pl List) String() string {
out := []string{}
for _, v := range pl {
out = append(out, fmt.Sprintf("%-3s %5d %-17s %#v",
v.Proto, v.Port, v.inode, v.Process))
}
return strings.Join(out, "\n")
}
func GetList(prev List) (List, error) {
pl, err := listPorts()
if err != nil {
return nil, fmt.Errorf("listPorts: %s", err)
}
if pl.SameInodes(prev) {
// Nothing changed, skip inode lookup
return prev, nil
}
pl, err = addProcesses(pl)
if err != nil {
return nil, fmt.Errorf("addProcesses: %s", err)
}
return pl, nil
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows
package portlist
import (
"bufio"
"bytes"
"fmt"
"log"
"os"
"strings"
exec "tailscale.com/tempfork/osexec"
)
// We have to run netstat, which is a bit expensive, so don't do it too often.
const POLL_SECONDS = 5
func listPorts() (List, error) {
return listPortsNetstat("-na")
}
// 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.
//
// TODO(apenwarr): this fails in a macOS sandbox (ie. our usual case).
// We might as well just delete this code if we can't find a solution.
func addProcesses(pl []Port) ([]Port, error) {
exe, err := exec.LookPath("lsof")
if err != nil {
return nil, fmt.Errorf("lsof: lookup: %v", err)
}
c := exec.Cmd{
Path: exe,
Args: []string{exe, "-F", "-n", "-P", "-O", "-S2", "-T", "-i4", "-i6"},
}
output, err := c.Output()
if err != nil {
xe, ok := err.(*exec.ExitError)
stderr := ""
if ok {
stderr = strings.TrimSpace(string(xe.Stderr))
}
// fails when run in a macOS sandbox, so make this non-fatal.
log.Printf("portlist: lsof: %v (%q)\n", err, stderr)
return pl, 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)
var cmd, proto string
for scanner.Scan() {
line := scanner.Text()
if line[0] == 'p' {
// starting a new process
cmd = ""
proto = ""
} else if line[0] == 'c' {
cmd = line[1:len(line)]
} else if line[0] == 'P' {
proto = strings.ToLower(line[1:len(line)])
} else if line[0] == 'n' {
rest := line[1:len(line)]
i := strings.Index(rest, "->")
if i < 0 {
// a listening port
port := parsePort(rest)
if port > 0 {
pp := ProtoPort{proto, uint16(port)}
p := m[pp]
if p != nil {
p.Process = cmd
} else {
fmt.Fprintf(os.Stderr, "weird: missing %v\n", pp)
}
}
}
}
}
return pl, nil
}

155
portlist/portlist_linux.go Normal file
View File

@@ -0,0 +1,155 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strconv"
"strings"
)
// Reading the sockfiles on Linux is very fast, so we can do it often.
const POLL_SECONDS = 1
// TODO(apenwarr): Include IPv6 ports eventually.
// Right now we don't route IPv6 anyway so it's better to exclude them.
var sockfiles = []string{"/proc/net/tcp", "/proc/net/udp"}
func listPorts() (List, error) {
l := []Port{}
for pi, fname := range sockfiles {
proto := protos[pi]
f, err := os.Open(fname)
if err != nil {
return nil, fmt.Errorf("%s: %s", fname, err)
}
defer f.Close()
r := bufio.NewReader(f)
// skip header row
_, err = r.ReadString('\n')
if err != nil {
return nil, err
}
for err == nil {
line, err := r.ReadString('\n')
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
// sl local rem ... inode
words := strings.Fields(line)
local := words[1]
rem := words[2]
inode := words[9]
if rem != "00000000:0000" {
// not a "listener" port
continue
}
portv, err := strconv.ParseUint(local[9:], 16, 16)
if err != nil {
return nil, fmt.Errorf("%#v: %s", local[9:], err)
}
inodev := fmt.Sprintf("socket:[%s]", inode)
l = append(l, Port{
Proto: proto,
Port: uint16(portv),
inode: inodev,
})
}
}
sort.Slice(l, func(i, j int) bool {
return (&l[i]).lessThan(&l[j])
})
return l, nil
}
func addProcesses(pl []Port) ([]Port, error) {
pm := map[string]*Port{}
for k := range pl {
pm[pl[k].inode] = &pl[k]
}
pdir, err := os.Open("/proc")
if err != nil {
return nil, fmt.Errorf("/proc: %s", err)
}
defer pdir.Close()
for {
pids, err := pdir.Readdirnames(100)
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("/proc: %s", err)
}
for _, pid := range pids {
_, err := strconv.ParseInt(pid, 10, 64)
if err != nil {
// not a pid, ignore it.
// /proc has lots of non-pid stuff in it.
continue
}
fddir, err := os.Open(fmt.Sprintf("/proc/%s/fd", pid))
if err != nil {
// Can't open fd list for this pid. Maybe
// don't have access. Ignore it.
continue
}
defer fddir.Close()
for {
fds, err := fddir.Readdirnames(100)
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("readdir: %s", err)
}
for _, fd := range fds {
target, err := os.Readlink(fmt.Sprintf("/proc/%s/fd/%s", pid, fd))
if err != nil {
// Not a symlink or no permission.
// Skip it.
continue
}
// TODO(apenwarr): use /proc/*/cmdline instead of /comm?
// Unsure right now whether users will want the extra detail
// or not.
pe := pm[target]
if pe != nil {
comm, err := ioutil.ReadFile(fmt.Sprintf("/proc/%s/comm", pid))
if err != nil {
// Usually shouldn't happen. One possibility is
// the process has gone away, so let's skip it.
continue
}
pe.Process = strings.TrimSpace(string(comm))
}
}
}
}
}
return pl, nil
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows,!darwin
package portlist
// We have to run netstat, which is a bit expensive, so don't do it too often.
const POLL_SECONDS = 5
func listPorts() (List, error) {
return listPortsNetstat("-na")
}
func addProcesses(pl []Port) ([]Port, error) {
// Generic version has no way to get process mappings.
// This has to be OS-specific.
return pl, nil
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
// Forking on Windows is insanely expensive, so don't do it too often.
const POLL_SECONDS = 5
func listPorts() (List, error) {
return listPortsNetstat("-na")
}
func addProcesses(pl []Port) ([]Port, error) {
return listPortsNetstat("-nab")
}