diff --git a/cmd/viewer/tests/tests_view.go b/cmd/viewer/tests/tests_view.go index 4b9bf475c..0c6c9e287 100644 --- a/cmd/viewer/tests/tests_view.go +++ b/cmd/viewer/tests/tests_view.go @@ -10,7 +10,6 @@ import ( "errors" "net/netip" - "go4.org/mem" "tailscale.com/types/views" ) @@ -312,7 +311,7 @@ func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf func (v StructWithSlicesView) Prefixes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Prefixes) } -func (v StructWithSlicesView) Data() mem.RO { return mem.B(v.ж.Data) } +func (v StructWithSlicesView) Data() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Data) } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct { diff --git a/cmd/viewer/viewer.go b/cmd/viewer/viewer.go index fa3d3d7b6..b9c01a9b8 100644 --- a/cmd/viewer/viewer.go +++ b/cmd/viewer/viewer.go @@ -67,7 +67,7 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error { {{end}} {{define "valueField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} { return v.ж.{{.FieldName}} } {{end}} -{{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() mem.RO { return mem.B(v.ж.{{.FieldName}}) } +{{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.ByteSlice[{{.FieldType}}] { return views.ByteSliceOf(v.ж.{{.FieldName}}) } {{end}} {{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) } {{end}} @@ -169,12 +169,12 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi case *types.Slice: slice := underlying elem := slice.Elem() - args.FieldType = it.QualifiedName(elem) switch elem.String() { case "byte": - it.Import("go4.org/mem") + args.FieldType = it.QualifiedName(fieldType) writeTemplate("byteSliceField") default: + args.FieldType = it.QualifiedName(elem) it.Import("tailscale.com/types/views") shallow, deep, base := requiresCloning(elem) if deep { diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index efdb3eaa0..368245676 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -73,12 +73,11 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) { // Not subject to tailnet lock. continue } - keySig := tkatype.MarshaledSignature(p.KeySignature().StringCopy()) // TODO(bradfitz,maisem): this is unfortunate. Change tkatype.MarshaledSignature to a string for viewer? - if len(keySig) == 0 { + if p.KeySignature().Len() == 0 { b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID(), p.StableID()) mak.Set(&toDelete, i, true) } else { - if err := b.tka.authority.NodeKeyAuthorized(p.Key(), keySig); err != nil { + if err := b.tka.authority.NodeKeyAuthorized(p.Key(), p.KeySignature().AsSlice()); err != nil { b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID(), p.StableID(), err) mak.Set(&toDelete, i, true) } diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index b3e9c145b..0b9250412 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -11,7 +11,6 @@ import ( "net/netip" "time" - "go4.org/mem" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -129,14 +128,16 @@ func (v *NodeView) UnmarshalJSON(b []byte) error { return nil } -func (v NodeView) ID() NodeID { return v.ж.ID } -func (v NodeView) StableID() StableNodeID { return v.ж.StableID } -func (v NodeView) Name() string { return v.ж.Name } -func (v NodeView) User() UserID { return v.ж.User } -func (v NodeView) Sharer() UserID { return v.ж.Sharer } -func (v NodeView) Key() key.NodePublic { return v.ж.Key } -func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry } -func (v NodeView) KeySignature() mem.RO { return mem.B(v.ж.KeySignature) } +func (v NodeView) ID() NodeID { return v.ж.ID } +func (v NodeView) StableID() StableNodeID { return v.ж.StableID } +func (v NodeView) Name() string { return v.ж.Name } +func (v NodeView) User() UserID { return v.ж.User } +func (v NodeView) Sharer() UserID { return v.ж.Sharer } +func (v NodeView) Key() key.NodePublic { return v.ж.Key } +func (v NodeView) KeyExpiry() time.Time { return v.ж.KeyExpiry } +func (v NodeView) KeySignature() views.ByteSlice[tkatype.MarshaledSignature] { + return views.ByteSliceOf(v.ж.KeySignature) +} func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine } func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey } func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) } @@ -610,13 +611,15 @@ func (v *RegisterResponseView) UnmarshalJSON(b []byte) error { return nil } -func (v RegisterResponseView) User() UserView { return v.ж.User.View() } -func (v RegisterResponseView) Login() Login { return v.ж.Login } -func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired } -func (v RegisterResponseView) MachineAuthorized() bool { return v.ж.MachineAuthorized } -func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL } -func (v RegisterResponseView) NodeKeySignature() mem.RO { return mem.B(v.ж.NodeKeySignature) } -func (v RegisterResponseView) Error() string { return v.ж.Error } +func (v RegisterResponseView) User() UserView { return v.ж.User.View() } +func (v RegisterResponseView) Login() Login { return v.ж.Login } +func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired } +func (v RegisterResponseView) MachineAuthorized() bool { return v.ж.MachineAuthorized } +func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL } +func (v RegisterResponseView) NodeKeySignature() views.ByteSlice[tkatype.MarshaledSignature] { + return views.ByteSliceOf(v.ж.NodeKeySignature) +} +func (v RegisterResponseView) Error() string { return v.ж.Error } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _RegisterResponseViewNeedsRegeneration = RegisterResponse(struct { @@ -749,8 +752,10 @@ func (v RegisterRequestView) Expiry() time.Time { return v.ж.Expir func (v RegisterRequestView) Followup() string { return v.ж.Followup } func (v RegisterRequestView) Hostinfo() HostinfoView { return v.ж.Hostinfo.View() } func (v RegisterRequestView) Ephemeral() bool { return v.ж.Ephemeral } -func (v RegisterRequestView) NodeKeySignature() mem.RO { return mem.B(v.ж.NodeKeySignature) } -func (v RegisterRequestView) SignatureType() SignatureType { return v.ж.SignatureType } +func (v RegisterRequestView) NodeKeySignature() views.ByteSlice[tkatype.MarshaledSignature] { + return views.ByteSliceOf(v.ж.NodeKeySignature) +} +func (v RegisterRequestView) SignatureType() SignatureType { return v.ж.SignatureType } func (v RegisterRequestView) Timestamp() *time.Time { if v.ж.Timestamp == nil { return nil @@ -759,8 +764,12 @@ func (v RegisterRequestView) Timestamp() *time.Time { return &x } -func (v RegisterRequestView) DeviceCert() mem.RO { return mem.B(v.ж.DeviceCert) } -func (v RegisterRequestView) Signature() mem.RO { return mem.B(v.ж.Signature) } +func (v RegisterRequestView) DeviceCert() views.ByteSlice[[]byte] { + return views.ByteSliceOf(v.ж.DeviceCert) +} +func (v RegisterRequestView) Signature() views.ByteSlice[[]byte] { + return views.ByteSliceOf(v.ж.Signature) +} // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _RegisterRequestViewNeedsRegeneration = RegisterRequest(struct { diff --git a/types/views/views.go b/types/views/views.go index 30bffcd74..3a802a09c 100644 --- a/types/views/views.go +++ b/types/views/views.go @@ -6,9 +6,12 @@ package views import ( + "bytes" "encoding/json" "errors" "maps" + + "go4.org/mem" ) func unmarshalSliceFromJSON[T any](b []byte, x *[]T) error { @@ -21,6 +24,83 @@ func unmarshalSliceFromJSON[T any](b []byte, x *[]T) error { return json.Unmarshal(b, x) } +// ByteSlice is a read-only accessor for types that are backed by a []byte. +type ByteSlice[T ~[]byte] struct { + // ж is the underlying mutable value, named with a hard-to-type + // character that looks pointy like a pointer. + // It is named distinctively to make you think of how dangerous it is to escape + // to callers. You must not let callers be able to mutate it. + ж T +} + +// ByteSliceOf returns a ByteSlice for the provided slice. +func ByteSliceOf[T ~[]byte](x T) ByteSlice[T] { + return ByteSlice[T]{x} +} + +// Len returns the length of the slice. +func (v ByteSlice[T]) Len() int { + return len(v.ж) +} + +// IsNil reports whether the underlying slice is nil. +func (v ByteSlice[T]) IsNil() bool { + return v.ж == nil +} + +// Mem returns a read-only view of the underlying slice. +func (v ByteSlice[T]) Mem() mem.RO { + return mem.B(v.ж) +} + +// Equal reports whether the underlying slice is equal to b. +func (v ByteSlice[T]) Equal(b T) bool { + return bytes.Equal(v.ж, b) +} + +// EqualView reports whether the underlying slice is equal to b. +func (v ByteSlice[T]) EqualView(b ByteSlice[T]) bool { + return bytes.Equal(v.ж, b.ж) +} + +// AsSlice returns a copy of the underlying slice. +func (v ByteSlice[T]) AsSlice() T { + return v.AppendTo(v.ж[:0:0]) +} + +// AppendTo appends the underlying slice values to dst. +func (v ByteSlice[T]) AppendTo(dst T) T { + return append(dst, v.ж...) +} + +// LenIter returns a slice the same length as the v.Len(). +// The caller can then range over it to get the valid indexes. +// It does not allocate. +func (v ByteSlice[T]) LenIter() []struct{} { return make([]struct{}, len(v.ж)) } + +// At returns the byte at index `i` of the slice. +func (v ByteSlice[T]) At(i int) byte { return v.ж[i] } + +// SliceFrom returns v[i:]. +func (v ByteSlice[T]) SliceFrom(i int) ByteSlice[T] { return ByteSlice[T]{v.ж[i:]} } + +// SliceTo returns v[:i] +func (v ByteSlice[T]) SliceTo(i int) ByteSlice[T] { return ByteSlice[T]{v.ж[:i]} } + +// Slice returns v[i:j] +func (v ByteSlice[T]) Slice(i, j int) ByteSlice[T] { return ByteSlice[T]{v.ж[i:j]} } + +// MarshalJSON implements json.Marshaler. +func (v ByteSlice[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) } + +// UnmarshalJSON implements json.Unmarshaler. +func (v *ByteSlice[T]) UnmarshalJSON(b []byte) error { + if v.ж != nil { + return errors.New("already initialized") + } + return json.Unmarshal(b, &v.ж) +} + // StructView represents the corresponding StructView of a Viewable. The concrete types are // typically generated by tailscale.com/cmd/viewer. type StructView[T any] interface { @@ -47,8 +127,9 @@ func SliceOfViews[T ViewCloner[T, V], V StructView[T]](x []T) SliceView[T, V] { return SliceView[T, V]{x} } -// SliceView is a read-only wrapper around a struct which should only be exposed -// as a View. +// SliceView wraps []T to provide accessors which return an immutable view V of +// T. It is used to provide the equivalent of SliceOf([]V) without having to +// allocate []V from []T. type SliceView[T ViewCloner[T, V], V StructView[T]] struct { // ж is the underlying mutable value, named with a hard-to-type // character that looks pointy like a pointer.