Compare commits

...

11 Commits

Author SHA1 Message Date
Denton Gentry
9bdaece3d7 VERSION.txt: this is v1.40.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-04-26 15:20:38 -07:00
Brad Fitzpatrick
1e876a3c1d ipn/ipnlocal: fix fmt format arg type mismatch in log line
It was printing like "v0xxxxxxxx" after version.Long became a func
in 8b2ae47c31.

Fixes #7976

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-26 14:28:09 -07:00
Maisem Ali
a8f10c23b2 cmd/tailscale/cli: [up] reuse --advertise-tags for OAuth key generation
We need to always specify tags when creating an AuthKey from an OAuth key.

Check for that, and reuse the `--advertise-tags` param.

Updates #7982

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-26 14:17:29 -07:00
Brad Fitzpatrick
b2b5379348 cmd/tailscale/cli: [up] change oauth authkey format
Updates #7982

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-26 13:36:17 -07:00
Brad Fitzpatrick
13de36303d cmd/tailscale/cli: [up] add experimental oauth2 authkey support
Updates #7982

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-04-26 12:25:42 -07:00
James Tucker
095d3edd33 ipn/ipnlocal: reenable profile tests on Windows
This fix does not seem ideal, but the test infrastructure using a local
goos doesn't seem to avoid all of the associated challenges, but is
somewhat deeply tied to the setup.

The core issue this addresses for now is that when run on Windows there
can be no code paths that attempt to use an invalid UID string, which on
Windows is described in [1].

For the goos="linux" tests, we now explicitly skip the affected
migration code if runtime.GOOS=="windows", and for the Windows test we
explicitly use the running users uid, rather than just the string
"user1". We also now make the case where a profile exists and has
already been migrated a non-error condition toward the outer API.

Updates #7876

[1] https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers

Signed-off-by: James Tucker <jftucker@gmail.com>
2023-04-26 11:33:38 -07:00
James Tucker
43819309e1 .github/workflows: split tests and benchmarks for caching
Benchmark flags prevent test caching, so benchmarks are now executed
independently of tests.

Fixes #7975

Signed-off-by: James Tucker <james@tailscale.com>
2023-04-26 10:49:38 -07:00
Maisem Ali
1b8a0dfe5e ssh/tailssh: also handle recording upload failure during writes
Previously we would error out when the recording server disappeared after the in memory
buffer filled up for the io.Copy. This makes it so that we handle failing open correctly
in that path.

Updates tailscale/corp#9967

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-04-25 19:40:46 -07:00
shayne
018a382729 cmd/tailscale/cli: [serve] fix MinGW path conversion (#7964)
Fixes #7963

Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-04-25 13:07:17 -04:00
License Updater
2e07245384 licenses: update android licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-04-25 09:10:26 -07:00
License Updater
aa87e999dc licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2023-04-25 09:10:05 -07:00
13 changed files with 205 additions and 56 deletions

View File

@@ -89,7 +89,11 @@ jobs:
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper
env:
GOARCH: ${{ matrix.goarch }}
- name: bench all
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper -test.bench=. -test.benchtime=1x -test.run=^$
env:
GOARCH: ${{ matrix.goarch }}
- name: check that no tracked files changed

View File

@@ -1 +1 @@
1.39.0
1.40.0

View File

@@ -29,9 +29,9 @@ func main() {
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
flag.Parse()
clientId := os.Getenv("TS_API_CLIENT_ID")
clientID := os.Getenv("TS_API_CLIENT_ID")
clientSecret := os.Getenv("TS_API_CLIENT_SECRET")
if clientId == "" || clientSecret == "" {
if clientID == "" || clientSecret == "" {
log.Fatal("TS_API_CLIENT_ID and TS_API_CLIENT_SECRET must be set")
}
@@ -39,22 +39,22 @@ func main() {
log.Fatal("at least one tag must be specified")
}
baseUrl := os.Getenv("TS_BASE_URL")
if baseUrl == "" {
baseUrl = "https://api.tailscale.com"
baseURL := os.Getenv("TS_BASE_URL")
if baseURL == "" {
baseURL = "https://api.tailscale.com"
}
credentials := clientcredentials.Config{
ClientID: clientId,
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: baseUrl + "/api/v2/oauth/token",
TokenURL: baseURL + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
ctx := context.Background()
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseUrl
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{

View File

@@ -16,6 +16,7 @@ import (
"path"
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
@@ -412,6 +413,7 @@ func cleanMountPoint(mount string) (string, error) {
if mount == "" {
return "", errors.New("mount point cannot be empty")
}
mount = cleanMinGWPathConversionIfNeeded(mount)
if !strings.HasPrefix(mount, "/") {
mount = "/" + mount
}
@@ -422,6 +424,26 @@ func cleanMountPoint(mount string) (string, error) {
return "", fmt.Errorf("invalid mount point %q", mount)
}
// cleanMinGWPathConversionIfNeeded strips the EXEPATH prefix from the given
// path if the path is a MinGW(ish) (Windows) shell arg.
//
// MinGW(ish) (Windows) shells perform POSIX-to-Windows path conversion
// converting the leading "/" of any shell arg to the EXEPATH, which mangles the
// mount point. Strip the EXEPATH prefix if it exists. #7963
//
// "/C:/Program Files/Git/foo" -> "/foo"
func cleanMinGWPathConversionIfNeeded(path string) string {
// Only do this on Windows.
if runtime.GOOS != "windows" {
return path
}
if _, ok := os.LookupEnv("MSYSTEM"); ok {
exepath := filepath.ToSlash(os.Getenv("EXEPATH"))
path = strings.TrimPrefix(path, exepath)
}
return path
}
func expandProxyTarget(source string) (string, error) {
if !strings.Contains(source, "://") {
source = "http://" + source

View File

@@ -13,11 +13,13 @@ import (
"fmt"
"log"
"net/netip"
"net/url"
"os"
"os/signal"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"syscall"
@@ -26,6 +28,9 @@ import (
shellquote "github.com/kballard/go-shellquote"
"github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -663,6 +668,10 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
if err != nil {
return err
}
authKey, err = resolveAuthKey(ctx, authKey, upArgs.advertiseTags)
if err != nil {
return err
}
if err := localClient.Start(ctx, ipn.Options{
AuthKey: authKey,
UpdatePrefs: prefs,
@@ -1102,3 +1111,96 @@ func anyPeerAdvertisingRoutes(st *ipnstate.Status) bool {
}
return false
}
func init() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
}
// resolveAuthKey either returns v unchanged (in the common case) or, if it
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
//
// tskey-client-xxxx[?ephemeral=false&bar&preauthorized=BOOL&baseURL=...]
//
// and does the OAuth2 dance to get and return an authkey. The "ephemeral"
// property defaults to true if unspecified. The "preauthorized" defaults to
// false. The "baseURL" defaults to https://api.tailscale.com.
// The passed in tags are required, and must be non-empty. These will be
// set on the authkey generated by the OAuth2 dance.
func resolveAuthKey(ctx context.Context, v, tags string) (string, error) {
if !strings.HasPrefix(v, "tskey-client-") {
return v, nil
}
if !envknob.Bool("TS_EXPERIMENT_OAUTH_AUTHKEY") {
return "", errors.New("oauth authkeys are in experimental status")
}
if tags == "" {
return "", errors.New("oauth authkeys require --advertise-tags")
}
clientSecret, named, _ := strings.Cut(v, "?")
attrs, err := url.ParseQuery(named)
if err != nil {
return "", err
}
for k := range attrs {
switch k {
case "ephemeral", "preauthorized", "baseURL":
default:
return "", fmt.Errorf("unknown attribute %q", k)
}
}
getBool := func(name string, def bool) (bool, error) {
v := attrs.Get(name)
if v == "" {
return def, nil
}
ret, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
}
return ret, nil
}
ephemeral, err := getBool("ephemeral", true)
if err != nil {
return "", err
}
preauth, err := getBool("preauthorized", false)
if err != nil {
return "", err
}
baseURL := "https://api.tailscale.com"
if v := attrs.Get("baseURL"); v != "" {
baseURL = v
}
credentials := clientcredentials.Config{
ClientID: "some-client-id", // ignored
ClientSecret: clientSecret,
TokenURL: baseURL + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: false,
Ephemeral: ephemeral,
Preauthorized: preauth,
Tags: strings.Split(tags, ","),
},
},
}
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
}
return authkey, nil
}

View File

@@ -155,6 +155,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/ipv6 from golang.org/x/net/icmp
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/tailscale/cli
golang.org/x/oauth2/internal from golang.org/x/oauth2+
golang.org/x/sync/errgroup from tailscale.com/derp+
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+

View File

@@ -2479,7 +2479,7 @@ func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineSt
// [GRINDER STATS LINES] - please don't remove (used for log parsing)
if peerStats.Len() > 0 {
b.keyLogf("[v1] peer keys: %s", strings.TrimSpace(peerKeys.String()))
b.statsLogf("[v1] v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
b.statsLogf("[v1] v%v peers: %v", version.Long(), strings.TrimSpace(peerStats.String()))
}
return ret
}

View File

@@ -22,6 +22,8 @@ import (
"tailscale.com/util/winutil"
)
var errAlreadyMigrated = errors.New("profile migration already completed")
// profileManager is a wrapper around a StateStore that manages
// multiple profiles and the current profile.
type profileManager struct {
@@ -66,7 +68,7 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error {
b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid)))
if err == ipn.ErrStateNotExist || len(b) == 0 {
if runtime.GOOS == "windows" {
if err := pm.migrateFromLegacyPrefs(); err != nil {
if err := pm.migrateFromLegacyPrefs(); err != nil && !errors.Is(err, errAlreadyMigrated) {
return err
}
} else {
@@ -544,7 +546,14 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, goos stri
if err := pm.setPrefsLocked(prefs); err != nil {
return nil, err
}
} else if len(knownProfiles) == 0 && goos != "windows" {
// Most platform behavior is controlled by the goos parameter, however
// some behavior is implied by build tag and fails when run on Windows,
// so we explicitly avoid that behavior when running on Windows.
// Specifically this reaches down into legacy preference loading that is
// specialized by profiles_windows.go and fails in tests on an invalid
// uid passed in from the unix tests. The uid's used for Windows tests
// and runtime must be valid Windows security identifier structures.
} else if len(knownProfiles) == 0 && goos != "windows" && runtime.GOOS != "windows" {
// No known profiles, try a migration.
if err := pm.migrateFromLegacyPrefs(); err != nil {
return nil, err
@@ -562,7 +571,7 @@ func (pm *profileManager) migrateFromLegacyPrefs() error {
sentinel, prefs, err := pm.loadLegacyPrefs()
if err != nil {
metricMigrationError.Add(1)
return err
return fmt.Errorf("load legacy prefs: %w", err)
}
if err := pm.SetPrefs(prefs); err != nil {
metricMigrationError.Add(1)

View File

@@ -5,7 +5,7 @@ package ipnlocal
import (
"fmt"
"runtime"
"os/user"
"strconv"
"testing"
@@ -18,9 +18,6 @@ import (
)
func TestProfileCurrentUserSwitch(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
}
store := new(mem.Store)
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
@@ -77,9 +74,6 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
}
func TestProfileList(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
}
store := new(mem.Store)
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
@@ -158,9 +152,6 @@ func TestProfileList(t *testing.T) {
// TestProfileManagement tests creating, loading, and switching profiles.
func TestProfileManagement(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
}
store := new(mem.Store)
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
@@ -312,10 +303,11 @@ func TestProfileManagement(t *testing.T) {
// TestProfileManagementWindows tests going into and out of Unattended mode on
// Windows.
func TestProfileManagementWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
u, err := user.Current()
if err != nil {
t.Fatal(err)
}
uid := ipn.WindowsUserID(u.Uid)
store := new(mem.Store)
@@ -365,8 +357,8 @@ func TestProfileManagementWindows(t *testing.T) {
{
t.Logf("Set user1 as logged in user")
if err := pm.SetCurrentUserID("user1"); err != nil {
t.Fatal(err)
if err := pm.SetCurrentUserID(uid); err != nil {
t.Fatalf("can't set user id: %s", err)
}
checkProfiles(t)
t.Logf("Save prefs for user1")
@@ -401,7 +393,7 @@ func TestProfileManagementWindows(t *testing.T) {
{
t.Logf("Set user1 as current user")
if err := pm.SetCurrentUserID("user1"); err != nil {
if err := pm.SetCurrentUserID(uid); err != nil {
t.Fatal(err)
}
wantCurProfile = "test"
@@ -411,8 +403,8 @@ func TestProfileManagementWindows(t *testing.T) {
t.Logf("set unattended mode")
wantProfiles["test"] = setPrefs(t, "test", true)
}
if pm.CurrentUserID() != "user1" {
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), "user1")
if pm.CurrentUserID() != uid {
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid)
}
// Recreate the profile manager to ensure that it starts with test profile.
@@ -421,7 +413,7 @@ func TestProfileManagementWindows(t *testing.T) {
t.Fatal(err)
}
checkProfiles(t)
if pm.CurrentUserID() != "user1" {
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), "user1")
if pm.CurrentUserID() != uid {
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid)
}
}

View File

@@ -6,6 +6,7 @@ package ipnlocal
import (
"errors"
"fmt"
"io/fs"
"os"
"os/user"
"path/filepath"
@@ -21,8 +22,6 @@ const (
legacyPrefsExt = ".conf"
)
var errAlreadyMigrated = errors.New("profile migration already completed")
func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) {
// TODO(aaron): Ideally we'd have the impersonation token for the pipe's
// client and use it to call SHGetKnownFolderPath, thus yielding the correct
@@ -56,6 +55,9 @@ func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
prefsPath := filepath.Join(userLegacyPrefsDir, legacyPrefsFile+legacyPrefsExt)
prefs, err := ipn.LoadPrefs(prefsPath)
if errors.Is(err, fs.ErrNotExist) {
return "", ipn.PrefsView{}, errAlreadyMigrated
}
if err != nil {
return "", ipn.PrefsView{}, err
}

View File

@@ -71,11 +71,11 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.5.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.8.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/a3b23cc7:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.5.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.7.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.6.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.6.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.8.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/162ed5ef888d/LICENSE))
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](https://github.com/inetaf/netaddr/blob/097006376321/LICENSE))

View File

@@ -32,7 +32,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE))
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f6f2f17d9da1/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/188cb8eef03f/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/ad93eed16885/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.1.6/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))

View File

@@ -1519,8 +1519,9 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
now := time.Now()
rec := &recording{
ss: ss,
start: now,
ss: ss,
start: now,
failOpen: onFailure == nil || onFailure.TerminateSessionWithMessage == "",
}
// We want to use a background context for uploading and not ss.ctx.
@@ -1611,6 +1612,10 @@ type recording struct {
ss *sshSession
start time.Time
// failOpen specifies whether the session should be allowed to
// continue if writing to the recording fails.
failOpen bool
mu sync.Mutex // guards writes to, close of out
out io.WriteCloser
}
@@ -1642,7 +1647,7 @@ func (r *recording) writer(dir string, w io.Writer) io.Writer {
// passwords.
return w
}
return &loggingWriter{r, dir, w}
return &loggingWriter{r: r, dir: dir, w: w}
}
// loggingWriter is an io.Writer wrapper that writes first an
@@ -1651,20 +1656,30 @@ type loggingWriter struct {
r *recording
dir string // "i" or "o" (input or output)
w io.Writer // underlying Writer, after writing to r.out
// recordingFailedOpen specifies whether we've failed to write to
// r.out and should stop trying. It is set to true if we fail to write
// to r.out and r.failOpen is set.
recordingFailedOpen bool
}
func (w loggingWriter) Write(p []byte) (n int, err error) {
j, err := json.Marshal([]any{
time.Since(w.r.start).Seconds(),
w.dir,
string(p),
})
if err != nil {
return 0, err
}
j = append(j, '\n')
if err := w.writeCastLine(j); err != nil {
return 0, err
func (w *loggingWriter) Write(p []byte) (n int, err error) {
if !w.recordingFailedOpen {
j, err := json.Marshal([]any{
time.Since(w.r.start).Seconds(),
w.dir,
string(p),
})
if err != nil {
return 0, err
}
j = append(j, '\n')
if err := w.writeCastLine(j); err != nil {
if !w.r.failOpen {
return 0, err
}
w.recordingFailedOpen = true
}
}
return w.w.Write(p)
}