diff --git a/portlist/portlist.go b/portlist/portlist.go index 122b81c03..aaff95279 100644 --- a/portlist/portlist.go +++ b/portlist/portlist.go @@ -52,7 +52,7 @@ func (a *Port) lessThan(b *Port) bool { } func (a List) sameInodes(b List) bool { - if a == nil || b == nil || len(a) != len(b) { + if len(a) != len(b) { return false } for i := range a { diff --git a/portlist/portlist_linux.go b/portlist/portlist_linux.go index 68d9c9de8..a40fb9909 100644 --- a/portlist/portlist_linux.go +++ b/portlist/portlist_linux.go @@ -6,6 +6,7 @@ package portlist import ( "bufio" + "bytes" "fmt" "io" "os" @@ -13,12 +14,14 @@ import ( "runtime" "strconv" "strings" + "sync" "sync/atomic" "syscall" "time" "go4.org/mem" "golang.org/x/sys/unix" + "tailscale.com/util/mak" ) // Reading the sockfiles on Linux is very fast, so we can do it often. @@ -35,13 +38,42 @@ const ( v4Any = "00000000:0000" ) +var eofReader = bytes.NewReader(nil) + +var bufioReaderPool = &sync.Pool{ + New: func() any { return bufio.NewReader(eofReader) }, +} + +type internedStrings struct { + m map[string]string +} + +func (v *internedStrings) get(b []byte) string { + if s, ok := v.m[string(b)]; ok { + return s + } + s := string(b) + mak.Set(&v.m, s, s) + return s +} + +var internedStringsPool = &sync.Pool{ + New: func() any { return new(internedStrings) }, +} + func appendListeningPorts(base []Port) ([]Port, error) { ret := base if sawProcNetPermissionErr.Load() { return ret, nil } - var br *bufio.Reader + br := bufioReaderPool.Get().(*bufio.Reader) + defer bufioReaderPool.Put(br) + defer br.Reset(eofReader) + + stringCache := internedStringsPool.Get().(*internedStrings) + defer internedStringsPool.Put(stringCache) + for _, fname := range sockfiles { // Android 10+ doesn't allow access to this anymore. // https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem @@ -59,25 +91,24 @@ func appendListeningPorts(base []Port) ([]Port, error) { if err != nil { return nil, fmt.Errorf("%s: %s", fname, err) } - if br == nil { - br = bufio.NewReader(f) - } else { - br.Reset(f) - } - ports, err := parsePorts(br, filepath.Base(fname)) + br.Reset(f) + ret, err = appendParsePorts(ret, stringCache, br, filepath.Base(fname)) f.Close() if err != nil { return nil, fmt.Errorf("parsing %q: %w", fname, err) } - ret = append(ret, ports...) + } + if len(stringCache.m) >= len(ret)*2 { + // Prevent unbounded growth of the internedStrings map. + stringCache.m = nil } return ret, nil } // fileBase is one of "tcp", "tcp6", "udp", "udp6". -func parsePorts(r *bufio.Reader, fileBase string) ([]Port, error) { +func appendParsePorts(base []Port, stringCache *internedStrings, r *bufio.Reader, fileBase string) ([]Port, error) { proto := strings.TrimSuffix(fileBase, "6") - var ret []Port + ret := base // skip header row _, err := r.ReadSlice('\n') @@ -171,7 +202,7 @@ func parsePorts(r *bufio.Reader, fileBase string) ([]Port, error) { ret = append(ret, Port{ Proto: proto, Port: uint16(portv), - inode: string(inoBuf), + inode: stringCache.get(inoBuf), }) } diff --git a/portlist/portlist_linux_test.go b/portlist/portlist_linux_test.go index d863cea0e..d48c004ec 100644 --- a/portlist/portlist_linux_test.go +++ b/portlist/portlist_linux_test.go @@ -76,6 +76,7 @@ func TestParsePorts(t *testing.T) { }, } + stringCache := new(internedStrings) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf := bytes.NewBufferString(tt.in) @@ -84,7 +85,7 @@ func TestParsePorts(t *testing.T) { if tt.file != "" { file = tt.file } - got, err := parsePorts(r, file) + got, err := appendParsePorts(nil, stringCache, r, file) if err != nil { t.Fatal(err) } @@ -116,11 +117,12 @@ func BenchmarkParsePorts(b *testing.B) { r := bytes.NewReader(contents.Bytes()) br := bufio.NewReader(&contents) + stringCache := new(internedStrings) b.ResetTimer() for i := 0; i < b.N; i++ { r.Seek(0, io.SeekStart) br.Reset(r) - got, err := parsePorts(br, "tcp6") + got, err := appendParsePorts(nil, stringCache, br, "tcp6") if err != nil { b.Fatal(err) }