diff --git a/portlist/portlist_linux.go b/portlist/portlist_linux.go index cc22b3f1a..0400be203 100644 --- a/portlist/portlist_linux.go +++ b/portlist/portlist_linux.go @@ -10,6 +10,7 @@ "io" "io/ioutil" "os" + "path/filepath" "runtime" "sort" "strconv" @@ -26,19 +27,25 @@ // 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"} -var protos = []string{"tcp", "udp"} +var sockfiles = []string{"/proc/net/tcp", "/proc/net/tcp6", "/proc/net/udp", "/proc/net/udp6"} var sawProcNetPermissionErr syncs.AtomicBool +const ( + v6Localhost = "00000000000000000000000001000000:" + v6Any = "00000000000000000000000000000000:0000" + v4Localhost = "0100007F:" + v4Any = "00000000:0000" +) + func listPorts() (List, error) { if sawProcNetPermissionErr.Get() { return nil, nil } l := []Port{} - for pi, fname := range sockfiles { - proto := protos[pi] + for _, fname := range sockfiles { + proto := strings.TrimSuffix(filepath.Base(fname), "6") // Android 10+ doesn't allow access to this anymore. // https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem @@ -59,47 +66,12 @@ func listPorts() (List, error) { defer f.Close() r := bufio.NewReader(f) - // skip header row - _, err = r.ReadString('\n') + ports, err := parsePorts(r, proto) if err != nil { - return nil, err + return nil, fmt.Errorf("parsing %q: %w", fname, 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 a port is bound to 127.0.0.1, ignore it. - if strings.HasPrefix(local, "0100007F:") { - continue - } - 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, - }) - } + l = append(l, ports...) } sort.Slice(l, func(i, j int) bool { @@ -109,6 +81,62 @@ func listPorts() (List, error) { return l, nil } +func parsePorts(r *bufio.Reader, proto string) ([]Port, error) { + var ret []Port + + // 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 a port is bound to localhost, ignore it. + // TODO: localhost is bigger than 1 IP, we need to ignore + // more things. + if strings.HasPrefix(local, v4Localhost) || strings.HasPrefix(local, v6Localhost) { + continue + } + if rem != v4Any && rem != v6Any { + // not a "listener" port + continue + } + + // Don't use strings.Split here, because it causes + // allocations significant enough to show up in profiles. + i := strings.IndexByte(local, ':') + if i == -1 { + return nil, fmt.Errorf("%q unexpectedly didn't have a colon", local) + } + portv, err := strconv.ParseUint(local[i+1:], 16, 16) + if err != nil { + return nil, fmt.Errorf("%#v: %s", local[9:], err) + } + inodev := fmt.Sprintf("socket:[%s]", inode) + ret = append(ret, Port{ + Proto: proto, + Port: uint16(portv), + inode: inodev, + }) + } + + return ret, nil +} + func addProcesses(pl []Port) ([]Port, error) { pm := map[string]*Port{} // by Port.inode for i := range pl { diff --git a/portlist/portlist_linux_test.go b/portlist/portlist_linux_test.go new file mode 100644 index 000000000..aac2028f9 --- /dev/null +++ b/portlist/portlist_linux_test.go @@ -0,0 +1,67 @@ +// 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" + "bytes" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParsePorts(t *testing.T) { + tests := []struct { + name string + in string + want []Port + }{ + { + name: "empty", + in: "header line (ignored)\n", + want: nil, + }, + { + name: "ipv4", + in: `header line + 0: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 22303 1 0000000000000000 100 0 0 10 0 + 1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 34062 1 0000000000000000 100 0 0 10 0 + 2: 5501A8C0:ADD4 B25E9536:01BB 01 00000000:00000000 02:00000B2B 00000000 1000 0 155276677 2 0000000000000000 22 4 30 10 -1 +`, + want: []Port{ + {Proto: "tcp", Port: 22, inode: "socket:[34062]"}, + }, + }, + { + name: "ipv6", + in: ` sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 00000000000000000000000001000000:0277 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 35720 1 0000000000000000 100 0 0 10 0 + 1: 00000000000000000000000000000000:1F91 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 142240557 1 0000000000000000 100 0 0 10 0 + 2: 00000000000000000000000000000000:0016 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 34064 1 0000000000000000 100 0 0 10 0 + 3: 69050120005716BC64906EBE009ECD4D:D506 0047062600000000000000006E171268:01BB 01 00000000:00000000 02:0000009E 00000000 1000 0 151042856 2 0000000000000000 21 4 28 10 -1 +`, + want: []Port{ + {Proto: "tcp", Port: 8081, inode: "socket:[142240557]"}, + {Proto: "tcp", Port: 22, inode: "socket:[34064]"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + buf := bytes.NewBufferString(test.in) + r := bufio.NewReader(buf) + + got, err := parsePorts(r, "tcp") + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, test.want, cmp.AllowUnexported(Port{})); diff != "" { + t.Errorf("unexpected parsed ports (-got+want):\n%s", diff) + } + }) + } +}