diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 248c0377e..dc558b36e 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -35,6 +35,7 @@ import ( "tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnstate" "tailscale.com/logtail" + "tailscale.com/net/neterror" "tailscale.com/net/netns" "tailscale.com/net/netutil" "tailscale.com/tailcfg" @@ -913,7 +914,9 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) { h.b.WatchNotificationsAs(ctx, h.Actor, mask, f.Flush, func(roNotify *ipn.Notify) (keepGoing bool) { err := enc.Encode(roNotify) if err != nil { - h.logf("json.Encode: %v", err) + if !neterror.IsClosedPipeError(err) { + h.logf("json.Encode: %v", err) + } return false } f.Flush() diff --git a/net/neterror/neterror_js.go b/net/neterror/neterror_js.go new file mode 100644 index 000000000..591367120 --- /dev/null +++ b/net/neterror/neterror_js.go @@ -0,0 +1,20 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build js || wasip1 || wasm + +package neterror + +import ( + "errors" + "io" + "io/fs" +) + +// Reports whether err resulted from reading or writing to a closed or broken pipe. +func IsClosedPipeError(err error) bool { + // Libraries may also return root errors like fs.ErrClosed/io.ErrClosedPipe + // due to a closed socket. + return errors.Is(err, fs.ErrClosed) || + errors.Is(err, io.ErrClosedPipe) +} diff --git a/net/neterror/neterror_plan9.go b/net/neterror/neterror_plan9.go new file mode 100644 index 000000000..a60c4dd64 --- /dev/null +++ b/net/neterror/neterror_plan9.go @@ -0,0 +1,24 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build plan9 + +package neterror + +import ( + "errors" + "io" + "io/fs" + "strings" +) + +// Reports whether err resulted from reading or writing to a closed or broken pipe. +func IsClosedPipeError(err error) bool { + // Libraries may also return root errors like fs.ErrClosed/io.ErrClosedPipe + // due to a closed socket. + // For a raw syscall error, check for error string containing "closed pipe", + // per the note set by the system: https://9p.io/magic/man2html/2/pipe + return errors.Is(err, fs.ErrClosed) || + errors.Is(err, io.ErrClosedPipe) || + strings.Contains(err.Error(), "closed pipe") +} diff --git a/net/neterror/neterror_posix.go b/net/neterror/neterror_posix.go new file mode 100644 index 000000000..71dda6b4c --- /dev/null +++ b/net/neterror/neterror_posix.go @@ -0,0 +1,32 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +//go:build !plan9 && !js && !wasip1 && !wasm + +package neterror + +import ( + "errors" + "io" + "io/fs" + "runtime" + "syscall" +) + +// Reports whether err resulted from reading or writing to a closed or broken pipe. +func IsClosedPipeError(err error) bool { + // 232 is Windows error code ERROR_NO_DATA, "The pipe is being closed". + if runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(232)) { + return true + } + + // EPIPE/ENOTCONN are common errors when a send fails due to a closed + // socket. There is some platform and version inconsistency in which + // error is returned, but the meaning is the same. + // Libraries may also return root errors like fs.ErrClosed/io.ErrClosedPipe + // due to a closed socket. + return errors.Is(err, syscall.EPIPE) || + errors.Is(err, syscall.ENOTCONN) || + errors.Is(err, fs.ErrClosed) || + errors.Is(err, io.ErrClosedPipe) +} diff --git a/wgengine/magicsock/magicsock_notplan9.go b/wgengine/magicsock/magicsock_notplan9.go index db2c5fca0..6bb9db5d7 100644 --- a/wgengine/magicsock/magicsock_notplan9.go +++ b/wgengine/magicsock/magicsock_notplan9.go @@ -8,6 +8,8 @@ package magicsock import ( "errors" "syscall" + + "tailscale.com/net/neterror" ) // shouldRebind returns if the error is one that is known to be healed by a @@ -17,7 +19,7 @@ func shouldRebind(err error) (ok bool, reason string) { // EPIPE/ENOTCONN are common errors when a send fails due to a closed // socket. There is some platform and version inconsistency in which // error is returned, but the meaning is the same. - case errors.Is(err, syscall.EPIPE), errors.Is(err, syscall.ENOTCONN): + case neterror.IsClosedPipeError(err): return true, "broken-pipe" // EPERM is typically caused by EDR software, and has been observed to be