summaryrefslogtreecommitdiffhomepage
path: root/portlist/netstat.go
blob: de625afb521702589e24a0389ca5aea383a09796 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// Copyright (c) Tailscale Inc & contributors
// 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
}