From 6a949d39e0c6fdb21c8ba07e6fbf876cfb676dfa Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 3 Feb 2022 09:07:40 -0800 Subject: [PATCH] net/interfaces: bound Linux /proc/net/route parsing tailscaled was using 100% CPU on a machine with ~1M lines, 100MB+ of /proc/net/route data. Two problems: in likelyHomeRouterIPLinux, we didn't stop reading the file once we found the default route (which is on the first non-header line when present). Which meant it was finding the answer and then parsing 100MB over 1M lines unnecessarily. Second was that if the default route isn't present, it'd read to the end of the file looking for it. If it's not in the first 1,000 lines, it ain't coming, or at least isn't worth having. (it's only used for discovering a potential UPnP/PMP/PCP server, which is very unlikely to be present in the environment of a machine with a ton of routes) Change-Id: I2c4a291ab7f26aedc13885d79237b8f05c2fd8e4 Signed-off-by: Brad Fitzpatrick (cherry picked from commit 2a67beaacfbeee8f9055c2e5d9200a1fa81dc94a) --- net/interfaces/interfaces_linux.go | 22 ++++++++++++++++++++-- net/interfaces/interfaces_linux_test.go | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/net/interfaces/interfaces_linux.go b/net/interfaces/interfaces_linux.go index 957a81161..83131a47f 100644 --- a/net/interfaces/interfaces_linux.go +++ b/net/interfaces/interfaces_linux.go @@ -28,6 +28,11 @@ func init() { var procNetRouteErr syncs.AtomicBool +// errStopReading is a sentinel error value used internally by +// lineread.File callers to stop reading. It doesn't escape to +// callers/users. +var errStopReading = errors.New("stop reading") + /* Parse 10.0.0.1 out of: @@ -47,12 +52,15 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) { } lineNum := 0 var f []mem.RO - err := lineread.File("/proc/net/route", func(line []byte) error { + err := lineread.File(procNetRoutePath, func(line []byte) error { lineNum++ if lineNum == 1 { // Skip header line. return nil } + if lineNum > maxProcNetRouteRead { + return errStopReading + } f = mem.AppendFields(f[:0], mem.B(line)) if len(f) < 4 { return nil @@ -74,9 +82,13 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) { ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24)) if ip.IsPrivate() { ret = ip + return errStopReading } return nil }) + if errors.Is(err, errStopReading) { + err = nil + } if err != nil { procNetRouteErr.Set(true) if runtime.GOOS == "android" { @@ -139,6 +151,10 @@ func defaultRoute() (d DefaultRouteDetails, err error) { var zeroRouteBytes = []byte("00000000") var procNetRoutePath = "/proc/net/route" +// maxProcNetRouteRead is the max number of lines to read from +// /proc/net/route looking for a default route. +const maxProcNetRouteRead = 1000 + func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) { f, err := os.Open(procNetRoutePath) if err != nil { @@ -147,9 +163,11 @@ func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) { defer f.Close() br := bufio.NewReaderSize(f, bufsize) + lineNum := 0 for { + lineNum++ line, err := br.ReadSlice('\n') - if err == io.EOF { + if err == io.EOF || lineNum > maxProcNetRouteRead { return "", fmt.Errorf("no default routes found: %w", err) } if err != nil { diff --git a/net/interfaces/interfaces_linux_test.go b/net/interfaces/interfaces_linux_test.go index 8ae48adad..2e362c084 100644 --- a/net/interfaces/interfaces_linux_test.go +++ b/net/interfaces/interfaces_linux_test.go @@ -51,7 +51,7 @@ func TestExtremelyLongProcNetRoute(t *testing.T) { t.Fatal(err) } - for n := 0; n <= 1000; n++ { + for n := 0; n <= 900; n++ { line := fmt.Sprintf("eth%d\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n", n) _, err := f.Write([]byte(line)) if err != nil {