WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
19 changed files with 100 additions and 46 deletions
Showing only changes of commit 82cfea90ca - Show all commits
+1 -1
View File
@@ -19,7 +19,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
💣 github.com/go4org/hashtriemap from tailscale.com/derp/derpserver
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/hdevalence/ed25519consensus from tailscale.com/tka
+1 -1
View File
@@ -41,7 +41,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext+
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail+
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
github.com/go-logr/logr from github.com/go-logr/logr/slogr+
github.com/go-logr/logr/slogr from github.com/go-logr/zapr
github.com/go-logr/zapr from sigs.k8s.io/controller-runtime/pkg/log/zap+
+1
View File
@@ -9,6 +9,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/v1 from tailscale.com/types/logger
github.com/munnerz/goautoneg from github.com/prometheus/common/expfmt
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
+1 -1
View File
@@ -125,7 +125,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
L 💣 github.com/godbus/dbus/v5 from fyne.io/systray+
L github.com/godbus/dbus/v5/introspect from fyne.io/systray+
L github.com/godbus/dbus/v5/prop from fyne.io/systray
+1
View File
@@ -15,6 +15,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/v1 from tailscale.com/types/logger
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
+1
View File
@@ -15,6 +15,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/v1 from tailscale.com/types/logger
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
+1 -1
View File
@@ -30,7 +30,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
+11 -26
View File
@@ -38,7 +38,7 @@ func run(pass *analysis.Pass) (any, error) {
if tag == "" {
continue
}
var seenName, hasFormat bool
var seenName bool
for opt := range strings.SplitSeq(tag, ",") {
if !seenName {
seenName = true
@@ -116,13 +116,18 @@ func run(pass *analysis.Pass) (any, error) {
report(pass, structType, fieldVar, StringOnNonNumericKind)
}
default:
key, _, ok := strings.Cut(opt, ":")
hasFormat = key == "format" && ok
// The `format` tag option did not make the cut for
// Go 1.27's encoding/json/v2. Its mere presence in a
// struct tag makes both v1 and v2 marshaling fail at
// runtime unless the caller passes the
// jsonv2.ExperimentalSupportFormatTag option.
// See https://go.dev/issue/71631 and
// https://github.com/tailscale/tailscale/issues/20528.
if key, _, ok := strings.Cut(opt, ":"); ok && key == "format" {
report(pass, structType, fieldVar, FormatUnsupported)
}
}
}
if !hasFormat && isTimeDuration(mayPointerElem(fieldVar.Type())) {
report(pass, structType, fieldVar, FormatMissingOnTimeDuration)
}
}
})
return nil, nil
@@ -152,26 +157,6 @@ func isString(t types.Type) bool {
return ok && basic.Kind() == types.String
}
// isTimeDuration reports whether t is a time.Duration type.
func isTimeDuration(t types.Type) bool {
return isNamed(t, "time", "Duration")
}
// mayPointerElem returns the pointed-at type if t is a pointer,
// otherwise it returns t as-is.
func mayPointerElem(t types.Type) types.Type {
if pointer, ok := t.(*types.Pointer); ok {
return pointer.Elem()
}
return t
}
// isNamed reports t is a named typed of the given path and name.
func isNamed(t types.Type, path, name string) bool {
gotPath, gotName := typeName(t)
return gotPath == path && gotName == name
}
// typeName reports the pkgPath and name of the type.
// It recursively follows type aliases to get the underlying named type.
func typeName(t types.Type) (pkgPath, name string) {
+3 -3
View File
@@ -74,7 +74,7 @@ const (
OmitEmptyShouldBeOmitZero ReportKind = "OmitEmptyShouldBeOmitZero"
OmitEmptyShouldBeOmitZeroButHasIsZero ReportKind = "OmitEmptyShouldBeOmitZeroButHasIsZero"
StringOnNonNumericKind ReportKind = "StringOnNonNumericKind"
FormatMissingOnTimeDuration ReportKind = "FormatMissingOnTimeDuration"
FormatUnsupported ReportKind = "FormatUnsupported"
)
func (k ReportKind) message() string {
@@ -89,8 +89,8 @@ func (k ReportKind) message() string {
return "should probably use `omitzero` instead of `omitempty`"
case StringOnNonNumericKind:
return "must not use `string` on non-numeric types"
case FormatMissingOnTimeDuration:
return "must use an explicit `format` tag (e.g., `format:nano`) on a time.Duration type; see https://go.dev/issue/71631"
case FormatUnsupported:
return "must not use the `format` tag option; Go 1.27's encoding/json rejects it at runtime (see tailscale/tailscale#20528)"
default:
return string(k)
}
+8 -1
View File
@@ -186,7 +186,14 @@ type Logtail struct {
// differently-shaped payloads in a single upload.
type LogEntry[T any] struct {
Logtail Logtail `json:"logtail,omitzero"`
Value T `json:",inline"`
// The `inline` tag option was renamed to `embed` in Go 1.27's
// encoding/json/v2, but the pinned go-json-experiment module
// (used on older Go versions) only knows `inline`. Each
// implementation ignores the option it doesn't know, so specify
// both until we require Go 1.27 and drop `inline`.
//
//lint:ignore SA5008 staticcheck doesn't know Go 1.27's `embed` option yet
Value T `json:",inline,embed"`
}
// UploadLogs uploads entries to the log server described by conf and returns
+1 -1
View File
@@ -28,7 +28,7 @@ type Logtail struct {
type LogEntry[T any] struct {
Logtail Logtail `json:"logtail,omitzero"`
Value T `json:",inline"`
Value T `json:",inline,embed"` // both options; see the non-omit variant in logtail.go
}
func UploadLogs[T any](ctx context.Context, conf Config, entries iter.Seq[LogEntry[T]]) error {
+1 -1
View File
@@ -24,7 +24,7 @@ const (
// conduct the test.
type config struct {
Version int `json:"version"`
TestDuration time.Duration `json:"time,format:nano"`
TestDuration time.Duration `json:"time"` // int64 nanoseconds; no jsonv2 format tag (tailscale/tailscale#20528)
Direction Direction `json:"direction"`
}
+6 -1
View File
@@ -3088,7 +3088,12 @@ type SSHAction struct {
// SessionDuration, if non-zero, is how long the session can stay open
// before being forcefully terminated.
SessionDuration time.Duration `json:"sessionDuration,omitempty,format:nano"`
// It is encoded as an int64 of nanoseconds (Go's time.Duration
// wire format for encoding/json v1). It must not use a jsonv2
// format tag; the mere presence of one makes Go 1.27's
// encoding/json fail to decode the struct. See
// https://github.com/tailscale/tailscale/issues/20528.
SessionDuration time.Duration `json:"sessionDuration,omitempty"`
// AllowAgentForwarding, if true, allows accepted connections to forward
// the ssh agent if requested.
+27
View File
@@ -1310,3 +1310,30 @@ func TestServiceActionTypeValid(t *testing.T) {
}
}
}
// TestSSHActionJSON verifies that SSHAction round-trips through
// encoding/json with SessionDuration encoded as int64 nanoseconds.
// It notably guards against jsonv2 `format` tag options in struct
// tags, which Go 1.27's encoding/json rejects at runtime.
// See https://github.com/tailscale/tailscale/issues/20528.
func TestSSHActionJSON(t *testing.T) {
a := SSHAction{
Accept: true,
SessionDuration: 5 * time.Second,
}
got, err := json.Marshal(a)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
const want = `{"accept":true,"sessionDuration":5000000000}`
if string(got) != want {
t.Errorf("Marshal = %s; want %s", got, want)
}
var back SSHAction
if err := json.Unmarshal(got, &back); err != nil {
t.Fatalf("Unmarshal: %v", err)
}
if !reflect.DeepEqual(back, a) {
t.Errorf("round trip = %+v; want %+v", back, a)
}
}
+5
View File
@@ -2107,6 +2107,11 @@ func (v SSHActionView) Accept() bool { return v.ж.Accept }
// SessionDuration, if non-zero, is how long the session can stay open
// before being forcefully terminated.
// It is encoded as an int64 of nanoseconds (Go's time.Duration
// wire format for encoding/json v1). It must not use a jsonv2
// format tag; the mere presence of one makes Go 1.27's
// encoding/json fail to decode the struct. See
// https://github.com/tailscale/tailscale/issues/20528.
func (v SSHActionView) SessionDuration() time.Duration { return v.ж.SessionDuration }
// AllowAgentForwarding, if true, allows accepted connections to forward
+1 -1
View File
@@ -30,7 +30,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck
github.com/go-json-experiment/json/v1 from tailscale.com/net/routecheck+
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
+6 -3
View File
@@ -21,6 +21,8 @@ import (
"context"
jsonv2 "github.com/go-json-experiment/json"
jsonv1 "github.com/go-json-experiment/json/v1"
"go4.org/mem"
"tailscale.com/envknob"
"tailscale.com/util/ctxkey"
@@ -379,12 +381,13 @@ func AsJSON(v any) fmt.Formatter {
type asJSONResult struct{ v any }
func (a asJSONResult) Format(s fmt.State, verb rune) {
v, err := json.Marshal(a.v)
// Write directly to s rather than going through json.Marshal's
// returned []byte to avoid an allocation. The explicit v1
// options keep the output identical to encoding/json.
err := jsonv2.MarshalWrite(s, a.v, jsonv1.DefaultOptionsV1())
if err != nil {
fmt.Fprintf(s, "%%!JSON-ERROR:%v", err)
return
}
s.Write(v)
}
// TestLogger returns a logger that logs to tb.Logf
+7 -2
View File
@@ -105,7 +105,11 @@ func (o Value[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
if !o.set {
return enc.WriteToken(jsontext.Null)
}
return jsonv2.MarshalEncode(enc, &o.value)
// Pin v2 semantics so the wire format (e.g. nil slices as "[]",
// nil maps as "{}") stays the same even when this method is
// reached via encoding/json v1, which as of Go 1.27 dispatches
// here with v1 options that would otherwise leak into this call.
return jsonv2.MarshalEncode(enc, &o.value, jsonv2.DefaultOptionsV2())
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
@@ -116,7 +120,8 @@ func (o *Value[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
return err
}
o.set = true
return jsonv2.UnmarshalDecode(dec, &o.value)
// Pin v2 semantics; see MarshalJSONTo.
return jsonv2.UnmarshalDecode(dec, &o.value, jsonv2.DefaultOptionsV2())
}
// MarshalJSON implements [json.Marshaler].
+17 -3
View File
@@ -52,7 +52,15 @@ type serializable[T any] struct {
// when the preference has not been configured.
Default T `json:",omitzero"`
// Metadata is any additional type-agnostic preference metadata to be serialized.
Metadata metadata `json:",inline"`
//
// The `inline` tag option was renamed to `embed` in Go 1.27's
// encoding/json/v2, but the pinned go-json-experiment module
// (used on older Go versions) only knows `inline`. Each
// implementation ignores the option it doesn't know, so specify
// both until we require Go 1.27 and drop `inline`.
//
//lint:ignore SA5008 staticcheck doesn't know Go 1.27's `embed` option yet
Metadata metadata `json:",inline,embed"`
}
// preference is an embeddable type that provides a common implementation for
@@ -165,12 +173,18 @@ var (
// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (p preference[T]) MarshalJSONTo(out *jsontext.Encoder) error {
return jsonv2.MarshalEncode(out, &p.s)
// Pin v2 semantics so the wire format (notably the handling of
// the `inline` and `omitzero` tag options in [serializable])
// stays the same even when this method is reached via
// encoding/json v1, which as of Go 1.27 dispatches here with v1
// options that would otherwise leak into this call.
return jsonv2.MarshalEncode(out, &p.s, jsonv2.DefaultOptionsV2())
}
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (p *preference[T]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
return jsonv2.UnmarshalDecode(in, &p.s)
// Pin v2 semantics; see MarshalJSONTo.
return jsonv2.UnmarshalDecode(in, &p.s, jsonv2.DefaultOptionsV2())
}
// MarshalJSON implements [json.Marshaler].