types/result, util/lineiter: add package for a result type, use it

This adds a new generic result type (motivated by golang/go#70084) to
try it out, and uses it in the new lineutil package (replacing the old
lineread package), changing that package to return iterators:
sometimes over []byte (when the input is all in memory), but sometimes
iterators over results of []byte, if errors might happen at runtime.

Updates #12912
Updates golang/go#70084

Change-Id: Iacdc1070e661b5fb163907b1e8b07ac7d51d3f83
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2024-11-04 20:49:40 -08:00 committed by Brad Fitzpatrick
parent 809a6eba80
commit 01185e436f
20 changed files with 290 additions and 139 deletions

View File

@ -140,6 +140,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/persist from tailscale.com/ipn tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/ipn tailscale.com/types/preftype from tailscale.com/ipn
tailscale.com/types/ptr from tailscale.com/hostinfo+ tailscale.com/types/ptr from tailscale.com/hostinfo+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/ipn+ tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+ tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/views from tailscale.com/ipn+ tailscale.com/types/views from tailscale.com/ipn+
@ -154,7 +155,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/fastuuid from tailscale.com/tsweb tailscale.com/util/fastuuid from tailscale.com/tsweb
💣 tailscale.com/util/hashx from tailscale.com/util/deephash 💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/health+ tailscale.com/util/mak from tailscale.com/health+
tailscale.com/util/multierr from tailscale.com/health+ tailscale.com/util/multierr from tailscale.com/health+

View File

@ -775,6 +775,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/types/persist from tailscale.com/control/controlclient+ tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+ tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/ptr from tailscale.com/cmd/k8s-operator+ tailscale.com/types/ptr from tailscale.com/cmd/k8s-operator+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/control/controlclient+ tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+ tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/views from tailscale.com/appc+ tailscale.com/types/views from tailscale.com/appc+
@ -792,7 +793,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/util/hashx from tailscale.com/util/deephash 💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+ tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
tailscale.com/util/httpm from tailscale.com/client/tailscale+ tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+ L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/appc+ tailscale.com/util/mak from tailscale.com/appc+
tailscale.com/util/multierr from tailscale.com/control/controlclient+ tailscale.com/util/multierr from tailscale.com/control/controlclient+

View File

@ -67,6 +67,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
tailscale.com/types/logger from tailscale.com/tsweb tailscale.com/types/logger from tailscale.com/tsweb
tailscale.com/types/opt from tailscale.com/envknob+ tailscale.com/types/opt from tailscale.com/envknob+
tailscale.com/types/ptr from tailscale.com/tailcfg+ tailscale.com/types/ptr from tailscale.com/tailcfg+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/tailcfg+ tailscale.com/types/structs from tailscale.com/tailcfg+
tailscale.com/types/tkatype from tailscale.com/tailcfg+ tailscale.com/types/tkatype from tailscale.com/tailcfg+
tailscale.com/types/views from tailscale.com/net/tsaddr+ tailscale.com/types/views from tailscale.com/net/tsaddr+
@ -74,7 +75,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/tailcfg tailscale.com/util/dnsname from tailscale.com/tailcfg
tailscale.com/util/fastuuid from tailscale.com/tsweb tailscale.com/util/fastuuid from tailscale.com/tsweb
tailscale.com/util/lineread from tailscale.com/version/distro tailscale.com/util/lineiter from tailscale.com/version/distro
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/slicesx from tailscale.com/tailcfg tailscale.com/util/slicesx from tailscale.com/tailcfg
tailscale.com/util/vizerror from tailscale.com/tailcfg+ tailscale.com/util/vizerror from tailscale.com/tailcfg+

View File

@ -148,6 +148,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/persist from tailscale.com/ipn tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+ tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/ptr from tailscale.com/hostinfo+ tailscale.com/types/ptr from tailscale.com/hostinfo+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/ipn+ tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/types/key+ tailscale.com/types/tkatype from tailscale.com/types/key+
tailscale.com/types/views from tailscale.com/tailcfg+ tailscale.com/types/views from tailscale.com/tailcfg+
@ -162,7 +163,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/groupmember from tailscale.com/client/web tailscale.com/util/groupmember from tailscale.com/client/web
💣 tailscale.com/util/hashx from tailscale.com/util/deephash 💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+ tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/cmd/tailscale/cli+ tailscale.com/util/mak from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+ tailscale.com/util/multierr from tailscale.com/control/controlhttp+

View File

@ -364,6 +364,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/persist from tailscale.com/control/controlclient+ tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+ tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/ptr from tailscale.com/control/controlclient+ tailscale.com/types/ptr from tailscale.com/control/controlclient+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/control/controlclient+ tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/tkatype from tailscale.com/tka+ tailscale.com/types/tkatype from tailscale.com/tka+
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+ tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
@ -381,7 +382,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/hashx from tailscale.com/util/deephash 💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+ tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
tailscale.com/util/httpm from tailscale.com/client/tailscale+ tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/lineiter from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+ L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+ tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+ tailscale.com/util/multierr from tailscale.com/cmd/tailscaled+

View File

@ -25,7 +25,7 @@
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/util/cloudenv" "tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
@ -231,12 +231,12 @@ func desktop() (ret opt.Bool) {
} }
seenDesktop := false seenDesktop := false
lineread.File("/proc/net/unix", func(line []byte) error { for lr := range lineiter.File("/proc/net/unix") {
line, _ := lr.Value()
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-")) seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix")) seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1")) seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
return nil }
})
ret.Set(seenDesktop) ret.Set(seenDesktop)
// Only cache after a minute - compositors might not have started yet. // Only cache after a minute - compositors might not have started yet.
@ -305,21 +305,21 @@ func inContainer() opt.Bool {
ret.Set(true) ret.Set(true)
return ret return ret
} }
lineread.File("/proc/1/cgroup", func(line []byte) error { for lr := range lineiter.File("/proc/1/cgroup") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("/docker/")) || if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) { mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret.Set(true) ret.Set(true)
return io.EOF // arbitrary non-nil error to stop loop break
} }
return nil }
}) for lr := range lineiter.File("/proc/mounts") {
lineread.File("/proc/mounts", func(line []byte) error { line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) { if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
ret.Set(true) ret.Set(true)
return io.EOF break
} }
return nil }
})
return ret return ret
} }

View File

@ -12,7 +12,7 @@
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
@ -106,15 +106,18 @@ func linuxVersionMeta() (meta versionMeta) {
} }
m := map[string]string{} m := map[string]string{}
lineread.File(propFile, func(line []byte) error { for lr := range lineiter.File(propFile) {
line, err := lr.Value()
if err != nil {
break
}
eq := bytes.IndexByte(line, '=') eq := bytes.IndexByte(line, '=')
if eq == -1 { if eq == -1 {
return nil continue
} }
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`) k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
m[k] = v m[k] = v
return nil }
})
if v := m["VERSION_CODENAME"]; v != "" { if v := m["VERSION_CODENAME"]; v != "" {
meta.DistroCodeName = v meta.DistroCodeName = v

View File

@ -27,7 +27,7 @@
"github.com/tailscale/golang-x-crypto/ssh" "github.com/tailscale/golang-x-crypto/ssh"
"go4.org/mem" "go4.org/mem"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
"tailscale.com/util/mak" "tailscale.com/util/mak"
) )
@ -80,30 +80,32 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
if err != nil { if err != nil {
return nil, err return nil, err
} }
lineread.Reader(bytes.NewReader(out), func(line []byte) error { for line := range lineiter.Bytes(out) {
line = bytes.TrimSpace(line) line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' { if len(line) == 0 || line[0] == '_' {
return nil continue
} }
add(string(line)) add(string(line))
return nil }
})
default: default:
lineread.File("/etc/passwd", func(line []byte) error { for lr := range lineiter.File("/etc/passwd") {
line, err := lr.Value()
if err != nil {
break
}
line = bytes.TrimSpace(line) line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' || line[0] == '_' { if len(line) == 0 || line[0] == '#' || line[0] == '_' {
return nil continue
} }
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) || if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) { mem.HasSuffix(mem.B(line), mem.S("/false")) {
return nil continue
} }
colon := bytes.IndexByte(line, ':') colon := bytes.IndexByte(line, ':')
if colon != -1 { if colon != -1 {
add(string(line[:colon])) add(string(line[:colon]))
} }
return nil }
})
} }
return res, nil return res, nil
} }

View File

@ -5,7 +5,6 @@
import ( import (
"bytes" "bytes"
"errors"
"log" "log"
"net/netip" "net/netip"
"os/exec" "os/exec"
@ -15,7 +14,7 @@
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/syncs" "tailscale.com/syncs"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
) )
var ( var (
@ -34,11 +33,6 @@ func init() {
var procNetRouteErr atomic.Bool var procNetRouteErr atomic.Bool
// 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:
@ -54,44 +48,42 @@ func likelyHomeRouterIPAndroid() (ret netip.Addr, myIP netip.Addr, ok bool) {
} }
lineNum := 0 lineNum := 0
var f []mem.RO var f []mem.RO
err := lineread.File(procNetRoutePath, func(line []byte) error { for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
}
lineNum++ lineNum++
if lineNum == 1 { if lineNum == 1 {
// Skip header line. // Skip header line.
return nil continue
} }
if lineNum > maxProcNetRouteRead { if lineNum > maxProcNetRouteRead {
return errStopReading break
} }
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 continue
} }
gwHex, flagsHex := f[2], f[3] gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16) flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil { if err != nil {
return nil // ignore error, skip line and keep going continue // ignore error, skip line and keep going
} }
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY { if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
return nil continue
} }
ipu32, err := mem.ParseUint(gwHex, 16, 32) ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil { if err != nil {
return nil // ignore error, skip line and keep going continue // ignore error, skip line and keep going
} }
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 break
} }
return nil
})
if errors.Is(err, errStopReading) {
err = nil
}
if err != nil {
procNetRouteErr.Store(true)
return likelyHomeRouterIP()
} }
if ret.IsValid() { if ret.IsValid() {
// Try to get the local IP of the interface associated with // Try to get the local IP of the interface associated with
@ -144,23 +136,26 @@ func likelyHomeRouterIPHelper() (ret netip.Addr, _ netip.Addr, ok bool) {
return return
} }
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 " // Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
lineread.Reader(out, func(line []byte) error { for lr := range lineiter.Reader(out) {
line, err := lr.Value()
if err != nil {
break
}
const pfx = "default via " const pfx = "default via "
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) { if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
return nil continue
} }
line = line[len(pfx):] line = line[len(pfx):]
sp := bytes.IndexByte(line, ' ') sp := bytes.IndexByte(line, ' ')
if sp == -1 { if sp == -1 {
return nil continue
} }
ipb := line[:sp] ipb := line[:sp]
if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() { if ip, err := netip.ParseAddr(string(ipb)); err == nil && ip.Is4() {
ret = ip ret = ip
log.Printf("interfaces: found Android default route %v", ip) log.Printf("interfaces: found Android default route %v", ip)
} }
return nil }
})
cmd.Process.Kill() cmd.Process.Kill()
cmd.Wait() cmd.Wait()
return ret, netip.Addr{}, ret.IsValid() return ret, netip.Addr{}, ret.IsValid()

View File

@ -4,14 +4,13 @@
package netmon package netmon
import ( import (
"errors"
"io" "io"
"net/netip" "net/netip"
"os/exec" "os/exec"
"testing" "testing"
"go4.org/mem" "go4.org/mem"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
"tailscale.com/version" "tailscale.com/version"
) )
@ -73,31 +72,34 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
defer io.Copy(io.Discard, stdout) // clear the pipe to prevent hangs defer io.Copy(io.Discard, stdout) // clear the pipe to prevent hangs
var f []mem.RO var f []mem.RO
lineread.Reader(stdout, func(lineb []byte) error { for lr := range lineiter.Reader(stdout) {
lineb, err := lr.Value()
if err != nil {
break
}
line := mem.B(lineb) line := mem.B(lineb)
if !mem.Contains(line, mem.S("default")) { if !mem.Contains(line, mem.S("default")) {
return nil continue
} }
f = mem.AppendFields(f[:0], line) f = mem.AppendFields(f[:0], line)
if len(f) < 4 || !f[0].EqualString("default") { if len(f) < 4 || !f[0].EqualString("default") {
return nil continue
} }
ipm, flagsm, netifm := f[1], f[2], f[3] ipm, flagsm, netifm := f[1], f[2], f[3]
if !mem.Contains(flagsm, mem.S("G")) { if !mem.Contains(flagsm, mem.S("G")) {
return nil continue
} }
if mem.Contains(flagsm, mem.S("I")) { if mem.Contains(flagsm, mem.S("I")) {
return nil continue
} }
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm))) ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
if err == nil && ip.IsPrivate() { if err == nil && ip.IsPrivate() {
ret = ip ret = ip
netif = netifm.StringCopy() netif = netifm.StringCopy()
// We've found what we're looking for. // We've found what we're looking for.
return errStopReadingNetstatTable break
} }
return nil }
})
return ret, netif, ret.IsValid() return ret, netif, ret.IsValid()
} }
@ -110,5 +112,3 @@ func TestFetchRoutingTable(t *testing.T) {
} }
} }
} }
var errStopReadingNetstatTable = errors.New("found private gateway")

View File

@ -23,7 +23,7 @@
"go4.org/mem" "go4.org/mem"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"tailscale.com/net/netaddr" "tailscale.com/net/netaddr"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
) )
func init() { func init() {
@ -32,11 +32,6 @@ func init() {
var procNetRouteErr atomic.Bool var procNetRouteErr atomic.Bool
// 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:
@ -52,44 +47,42 @@ func likelyHomeRouterIPLinux() (ret netip.Addr, myIP netip.Addr, ok bool) {
} }
lineNum := 0 lineNum := 0
var f []mem.RO var f []mem.RO
err := lineread.File(procNetRoutePath, func(line []byte) error { for lr := range lineiter.File(procNetRoutePath) {
line, err := lr.Value()
if err != nil {
procNetRouteErr.Store(true)
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
return ret, myIP, false
}
lineNum++ lineNum++
if lineNum == 1 { if lineNum == 1 {
// Skip header line. // Skip header line.
return nil continue
} }
if lineNum > maxProcNetRouteRead { if lineNum > maxProcNetRouteRead {
return errStopReading break
} }
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 continue
} }
gwHex, flagsHex := f[2], f[3] gwHex, flagsHex := f[2], f[3]
flags, err := mem.ParseUint(flagsHex, 16, 16) flags, err := mem.ParseUint(flagsHex, 16, 16)
if err != nil { if err != nil {
return nil // ignore error, skip line and keep going continue // ignore error, skip line and keep going
} }
if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY { if flags&(unix.RTF_UP|unix.RTF_GATEWAY) != unix.RTF_UP|unix.RTF_GATEWAY {
return nil continue
} }
ipu32, err := mem.ParseUint(gwHex, 16, 32) ipu32, err := mem.ParseUint(gwHex, 16, 32)
if err != nil { if err != nil {
return nil // ignore error, skip line and keep going continue // ignore error, skip line and keep going
} }
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 break
} }
return nil
})
if errors.Is(err, errStopReading) {
err = nil
}
if err != nil {
procNetRouteErr.Store(true)
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
} }
if ret.IsValid() { if ret.IsValid() {
// Try to get the local IP of the interface associated with // Try to get the local IP of the interface associated with

View File

@ -1,6 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package netmon package netmon
import ( import (

View File

@ -17,7 +17,7 @@
"sync" "sync"
"time" "time"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
) )
// These vars are overridden for tests. // These vars are overridden for tests.
@ -76,21 +76,22 @@ func synologyProxiesFromConfig() (*url.URL, *url.URL, error) {
func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) { func parseSynologyConfig(r io.Reader) (*url.URL, *url.URL, error) {
cfg := map[string]string{} cfg := map[string]string{}
if err := lineread.Reader(r, func(line []byte) error { for lr := range lineiter.Reader(r) {
line, err := lr.Value()
if err != nil {
return nil, nil, err
}
// accept and skip over empty lines // accept and skip over empty lines
line = bytes.TrimSpace(line) line = bytes.TrimSpace(line)
if len(line) == 0 { if len(line) == 0 {
return nil continue
} }
key, value, ok := strings.Cut(string(line), "=") key, value, ok := strings.Cut(string(line), "=")
if !ok { if !ok {
return fmt.Errorf("missing \"=\" in proxy.conf line: %q", line) return nil, nil, fmt.Errorf("missing \"=\" in proxy.conf line: %q", line)
} }
cfg[string(key)] = string(value) cfg[string(key)] = string(value)
return nil
}); err != nil {
return nil, nil, err
} }
if cfg["proxy_enabled"] != "yes" { if cfg["proxy_enabled"] != "yes" {

View File

@ -48,7 +48,7 @@
"tailscale.com/types/netmap" "tailscale.com/types/netmap"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/util/cibuild" "tailscale.com/util/cibuild"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
"tailscale.com/util/must" "tailscale.com/util/must"
"tailscale.com/version/distro" "tailscale.com/version/distro"
"tailscale.com/wgengine" "tailscale.com/wgengine"
@ -1123,14 +1123,11 @@ func TestSSH(t *testing.T) {
func parseEnv(out []byte) map[string]string { func parseEnv(out []byte) map[string]string {
e := map[string]string{} e := map[string]string{}
lineread.Reader(bytes.NewReader(out), func(line []byte) error { for line := range lineiter.Bytes(out) {
i := bytes.IndexByte(line, '=') if i := bytes.IndexByte(line, '='); i != -1 {
if i == -1 { e[string(line[:i])] = string(line[i+1:])
return nil
} }
e[string(line[:i])] = string(line[i+1:]) }
return nil
})
return e return e
} }

View File

@ -6,7 +6,6 @@
package tailssh package tailssh
import ( import (
"io"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
@ -18,7 +17,7 @@
"go4.org/mem" "go4.org/mem"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/hostinfo" "tailscale.com/hostinfo"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
"tailscale.com/util/osuser" "tailscale.com/util/osuser"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
@ -110,15 +109,16 @@ func defaultPathForUser(u *user.User) string {
} }
func defaultPathForUserOnNixOS(u *user.User) string { func defaultPathForUserOnNixOS(u *user.User) string {
var path string for lr := range lineiter.File("/etc/pam/environment") {
lineread.File("/etc/pam/environment", func(lineb []byte) error { lineb, err := lr.Value()
if v := pathFromPAMEnvLine(lineb, u); v != "" { if err != nil {
path = v return ""
return io.EOF // stop iteration
} }
return nil if v := pathFromPAMEnvLine(lineb, u); v != "" {
}) return v
return path }
}
return ""
} }
func pathFromPAMEnvLine(line []byte, u *user.User) (path string) { func pathFromPAMEnvLine(line []byte, u *user.User) (path string) {

49
types/result/result.go Normal file
View File

@ -0,0 +1,49 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package result contains the Of result type, which is
// either a value or an error.
package result
// Of is either a T value or an error.
//
// Think of it like Rust or Swift's result types.
// It's named "Of" because the fully qualified name
// for callers reads result.Of[T].
type Of[T any] struct {
v T // valid if Err is nil; invalid if Err is non-nil
err error
}
// Value returns a new result with value v,
// without an error.
func Value[T any](v T) Of[T] {
return Of[T]{v: v}
}
// Error returns a new result with error err.
// If err is nil, the returned result is equivalent
// to calling Value with T's zero value.
func Error[T any](err error) Of[T] {
return Of[T]{err: err}
}
// MustValue returns r's result value.
// It panics if r.Err returns non-nil.
func (r Of[T]) MustValue() T {
if r.err != nil {
panic(r.err)
}
return r.v
}
// Value returns r's result value and error.
func (r Of[T]) Value() (T, error) {
return r.v, r.err
}
// Err returns r's error, if any.
// When r.Err returns nil, it's safe to call r.MustValue without it panicking.
func (r Of[T]) Err() error {
return r.err
}

72
util/lineiter/lineiter.go Normal file
View File

@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package lineiter iterates over lines in things.
package lineiter
import (
"bufio"
"bytes"
"io"
"iter"
"os"
"tailscale.com/types/result"
)
// File returns an iterator that reads lines from the named file.
//
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func File(name string) iter.Seq[result.Of[[]byte]] {
f, err := os.Open(name)
return reader(f, f, err)
}
// Bytes returns an iterator over the lines in bs.
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func Bytes(bs []byte) iter.Seq[[]byte] {
return func(yield func([]byte) bool) {
for len(bs) > 0 {
i := bytes.IndexByte(bs, '\n')
if i < 0 {
yield(bs)
return
}
if !yield(bs[:i]) {
return
}
bs = bs[i+1:]
}
}
}
// Reader returns an iterator over the lines in r.
//
// The returned substrings don't include the trailing newline.
// Lines may be empty.
func Reader(r io.Reader) iter.Seq[result.Of[[]byte]] {
return reader(r, nil, nil)
}
func reader(r io.Reader, c io.Closer, err error) iter.Seq[result.Of[[]byte]] {
return func(yield func(result.Of[[]byte]) bool) {
if err != nil {
yield(result.Error[[]byte](err))
return
}
if c != nil {
defer c.Close()
}
bs := bufio.NewScanner(r)
for bs.Scan() {
if !yield(result.Value(bs.Bytes())) {
return
}
}
if err := bs.Err(); err != nil {
yield(result.Error[[]byte](err))
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package lineiter
import (
"slices"
"strings"
"testing"
)
func TestBytesLines(t *testing.T) {
var got []string
for line := range Bytes([]byte("foo\n\nbar\nbaz")) {
got = append(got, string(line))
}
want := []string{"foo", "", "bar", "baz"}
if !slices.Equal(got, want) {
t.Errorf("got %q; want %q", got, want)
}
}
func TestReader(t *testing.T) {
var got []string
for line := range Reader(strings.NewReader("foo\n\nbar\nbaz")) {
got = append(got, string(line.MustValue()))
}
want := []string{"foo", "", "bar", "baz"}
if !slices.Equal(got, want) {
t.Errorf("got %q; want %q", got, want)
}
}

View File

@ -8,26 +8,26 @@
"os" "os"
"strings" "strings"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
) )
func ownerOfPID(pid int) (userID string, err error) { func ownerOfPID(pid int) (userID string, err error) {
file := fmt.Sprintf("/proc/%d/status", pid) file := fmt.Sprintf("/proc/%d/status", pid)
err = lineread.File(file, func(line []byte) error { for lr := range lineiter.File(file) {
line, err := lr.Value()
if err != nil {
if os.IsNotExist(err) {
return "", ErrProcessNotFound
}
return "", err
}
if len(line) < 4 || string(line[:4]) != "Uid:" { if len(line) < 4 || string(line[:4]) != "Uid:" {
return nil continue
} }
f := strings.Fields(string(line)) f := strings.Fields(string(line))
if len(f) >= 2 { if len(f) >= 2 {
userID = f[1] // real userid userID = f[1] // real userid
} }
return nil
})
if os.IsNotExist(err) {
return "", ErrProcessNotFound
}
if err != nil {
return
} }
if userID == "" { if userID == "" {
return "", fmt.Errorf("missing Uid line in %s", file) return "", fmt.Errorf("missing Uid line in %s", file)

View File

@ -6,13 +6,12 @@
import ( import (
"bytes" "bytes"
"io"
"os" "os"
"runtime" "runtime"
"strconv" "strconv"
"tailscale.com/types/lazy" "tailscale.com/types/lazy"
"tailscale.com/util/lineread" "tailscale.com/util/lineiter"
) )
type Distro string type Distro string
@ -132,18 +131,19 @@ func DSMVersion() int {
return v return v
} }
// But when run from the command line, we have to read it from the file: // But when run from the command line, we have to read it from the file:
lineread.File("/etc/VERSION", func(line []byte) error { for lr := range lineiter.File("/etc/VERSION") {
line, err := lr.Value()
if err != nil {
break // but otherwise ignore
}
line = bytes.TrimSpace(line) line = bytes.TrimSpace(line)
if string(line) == `majorversion="7"` { if string(line) == `majorversion="7"` {
v = 7 return 7
return io.EOF
} }
if string(line) == `majorversion="6"` { if string(line) == `majorversion="6"` {
v = 6 return 6
return io.EOF
} }
return nil }
}) return 0
return v
}) })
} }