mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-01 09:32:08 +00:00
Move Linux client & common packages into a public repo.
This commit is contained in:
155
portlist/netstat.go
Normal file
155
portlist/netstat.go
Normal 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
89
portlist/netstat_test.go
Normal 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
59
portlist/poller.go
Normal 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
87
portlist/portlist.go
Normal 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
|
||||
}
|
||||
99
portlist/portlist_darwin.go
Normal file
99
portlist/portlist_darwin.go
Normal 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
155
portlist/portlist_linux.go
Normal 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
|
||||
}
|
||||
20
portlist/portlist_other.go
Normal file
20
portlist/portlist_other.go
Normal 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
|
||||
}
|
||||
16
portlist/portlist_windows.go
Normal file
16
portlist/portlist_windows.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user