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 <bradfitz@tailscale.com>
pull/3870/head
Brad Fitzpatrick 3 years ago committed by Brad Fitzpatrick
parent 0626cf4183
commit 2a67beaacf

@ -28,6 +28,11 @@ func init() {
var procNetRouteErr syncs.AtomicBool 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: Parse 10.0.0.1 out of:
@ -47,12 +52,15 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
} }
lineNum := 0 lineNum := 0
var f []mem.RO var f []mem.RO
err := lineread.File("/proc/net/route", func(line []byte) error { err := lineread.File(procNetRoutePath, func(line []byte) error {
lineNum++ lineNum++
if lineNum == 1 { if lineNum == 1 {
// Skip header line. // Skip header line.
return nil return nil
} }
if lineNum > maxProcNetRouteRead {
return errStopReading
}
f = mem.AppendFields(f[:0], mem.B(line)) f = mem.AppendFields(f[:0], mem.B(line))
if len(f) < 4 { if len(f) < 4 {
return nil 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)) ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
if ip.IsPrivate() { if ip.IsPrivate() {
ret = ip ret = ip
return errStopReading
} }
return nil return nil
}) })
if errors.Is(err, errStopReading) {
err = nil
}
if err != nil { if err != nil {
procNetRouteErr.Set(true) procNetRouteErr.Set(true)
if runtime.GOOS == "android" { if runtime.GOOS == "android" {
@ -139,6 +151,10 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
var zeroRouteBytes = []byte("00000000") var zeroRouteBytes = []byte("00000000")
var procNetRoutePath = "/proc/net/route" 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) { func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
f, err := os.Open(procNetRoutePath) f, err := os.Open(procNetRoutePath)
if err != nil { if err != nil {
@ -147,9 +163,11 @@ func defaultRouteInterfaceProcNetInternal(bufsize int) (string, error) {
defer f.Close() defer f.Close()
br := bufio.NewReaderSize(f, bufsize) br := bufio.NewReaderSize(f, bufsize)
lineNum := 0
for { for {
lineNum++
line, err := br.ReadSlice('\n') line, err := br.ReadSlice('\n')
if err == io.EOF { if err == io.EOF || lineNum > maxProcNetRouteRead {
return "", fmt.Errorf("no default routes found: %w", err) return "", fmt.Errorf("no default routes found: %w", err)
} }
if err != nil { if err != nil {

@ -51,7 +51,7 @@ func TestExtremelyLongProcNetRoute(t *testing.T) {
t.Fatal(err) 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) line := fmt.Sprintf("eth%d\t8008FEA9\t00000000\t0001\t0\t0\t0\t01FFFFFF\t0\t0\t0\n", n)
_, err := f.Write([]byte(line)) _, err := f.Write([]byte(line))
if err != nil { if err != nil {

Loading…
Cancel
Save