You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/portlist/netstat.go

148 lines
3.3 KiB
Go

// 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.
//go:build !ios && !js
package portlist
import (
"sort"
"strconv"
"strings"
)
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:]
if portstr == "*" {
return 0
}
port, err := strconv.ParseUint(portstr, 10, 16)
if err != nil {
// invalid port; weird
return -1
}
return int(port)
}
func isLoopbackAddr(s string) bool {
return strings.HasPrefix(s, "127.") ||
strings.HasPrefix(s, "[::1]:") ||
strings.HasPrefix(s, "::1.")
}
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 appendParsePortsNetstat(base []Port, output string) []Port {
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
}
if isLoopbackAddr(laddr) {
// not interested in loopback-bound listeners
continue
}
} else if strings.HasPrefix(protos, "udp") {
if len(cols) < 3 {
continue
}
proto = "udp"
laddr = cols[len(cols)-2]
raddr = cols[len(cols)-1]
if isLoopbackAddr(laddr) {
// not interested in loopback-bound listeners
continue
}
} 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 = argvSubject(lastline)
} else {
p.Process = argvSubject(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 = ""
}
ret := base
for p := range m {
ret = append(ret, p)
}
// Only sort the part we appended. It's up to the caller to sort the whole
// thing if they'd like. In practice the caller's base will have len 0,
// though, so the whole thing will be sorted.
toSort := ret[len(base):]
sort.Slice(toSort, func(i, j int) bool {
return (&toSort[i]).lessThan(&toSort[j])
})
return ret
}