diff --git a/cmd/viewer/tests/tests_view.go b/cmd/viewer/tests/tests_view.go index bc95fea01..e50a71c9e 100644 --- a/cmd/viewer/tests/tests_view.go +++ b/cmd/viewer/tests/tests_view.go @@ -247,47 +247,41 @@ func (v *MapView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v MapView) Int() views.Map[string, int] { return views.MapOf(v.ж.Int) } - +func (v MapView) Int() views.Map[string, int] { return views.MapOf(v.ж.Int) } func (v MapView) SliceInt() views.MapSlice[string, int] { return views.MapSliceOf(v.ж.SliceInt) } - func (v MapView) StructPtrWithPtr() views.MapFn[string, *StructWithPtrs, StructWithPtrsView] { return views.MapFnOf(v.ж.StructPtrWithPtr, func(t *StructWithPtrs) StructWithPtrsView { return t.View() }) } - func (v MapView) StructPtrWithoutPtr() views.MapFn[string, *StructWithoutPtrs, StructWithoutPtrsView] { return views.MapFnOf(v.ж.StructPtrWithoutPtr, func(t *StructWithoutPtrs) StructWithoutPtrsView { return t.View() }) } - func (v MapView) StructWithoutPtr() views.Map[string, StructWithoutPtrs] { return views.MapOf(v.ж.StructWithoutPtr) } - func (v MapView) SlicesWithPtrs() views.MapFn[string, []*StructWithPtrs, views.SliceView[*StructWithPtrs, StructWithPtrsView]] { return views.MapFnOf(v.ж.SlicesWithPtrs, func(t []*StructWithPtrs) views.SliceView[*StructWithPtrs, StructWithPtrsView] { return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](t) }) } - func (v MapView) SlicesWithoutPtrs() views.MapFn[string, []*StructWithoutPtrs, views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView]] { return views.MapFnOf(v.ж.SlicesWithoutPtrs, func(t []*StructWithoutPtrs) views.SliceView[*StructWithoutPtrs, StructWithoutPtrsView] { return views.SliceOfViews[*StructWithoutPtrs, StructWithoutPtrsView](t) }) } - func (v MapView) StructWithoutPtrKey() views.Map[StructWithoutPtrs, int] { return views.MapOf(v.ж.StructWithoutPtrKey) } - func (v MapView) StructWithPtr() views.MapFn[string, StructWithPtrs, StructWithPtrsView] { return views.MapFnOf(v.ж.StructWithPtr, func(t StructWithPtrs) StructWithPtrsView { return t.View() }) } + +// Unsupported views. func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported") } func (v MapView) PointerKey() map[*string]int { panic("unsupported") } func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") } @@ -389,8 +383,10 @@ func (v StructWithSlicesView) Prefixes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Prefixes) } func (v StructWithSlicesView) Data() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Data) } -func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") } -func (v StructWithSlicesView) Ints() *int { panic("unsupported") } + +// Unsupported views. +func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") } +func (v StructWithSlicesView) Ints() *int { panic("unsupported") } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct { @@ -554,9 +550,10 @@ func (v GenericIntStructView[T]) Pointer() views.ValuePointer[T] { return views.ValuePointerOf(v.ж.Pointer) } -func (v GenericIntStructView[T]) Slice() views.Slice[T] { return views.SliceOf(v.ж.Slice) } +func (v GenericIntStructView[T]) Slice() views.Slice[T] { return views.SliceOf(v.ж.Slice) } +func (v GenericIntStructView[T]) Map() views.Map[string, T] { return views.MapOf(v.ж.Map) } -func (v GenericIntStructView[T]) Map() views.Map[string, T] { return views.MapOf(v.ж.Map) } +// Unsupported views. func (v GenericIntStructView[T]) PtrSlice() *T { panic("unsupported") } func (v GenericIntStructView[T]) PtrKeyMap() map[*T]string { panic("unsupported") } func (v GenericIntStructView[T]) PtrValueMap() map[string]*T { panic("unsupported") } @@ -648,9 +645,10 @@ func (v GenericNoPtrsStructView[T]) Pointer() views.ValuePointer[T] { return views.ValuePointerOf(v.ж.Pointer) } -func (v GenericNoPtrsStructView[T]) Slice() views.Slice[T] { return views.SliceOf(v.ж.Slice) } +func (v GenericNoPtrsStructView[T]) Slice() views.Slice[T] { return views.SliceOf(v.ж.Slice) } +func (v GenericNoPtrsStructView[T]) Map() views.Map[string, T] { return views.MapOf(v.ж.Map) } -func (v GenericNoPtrsStructView[T]) Map() views.Map[string, T] { return views.MapOf(v.ж.Map) } +// Unsupported views. func (v GenericNoPtrsStructView[T]) PtrSlice() *T { panic("unsupported") } func (v GenericNoPtrsStructView[T]) PtrKeyMap() map[*T]string { panic("unsupported") } func (v GenericNoPtrsStructView[T]) PtrValueMap() map[string]*T { panic("unsupported") } @@ -741,12 +739,13 @@ func (v GenericCloneableStructView[T, V]) Value() V { return v.ж.Value.View() } func (v GenericCloneableStructView[T, V]) Slice() views.SliceView[T, V] { return views.SliceOfViews[T, V](v.ж.Slice) } - func (v GenericCloneableStructView[T, V]) Map() views.MapFn[string, T, V] { return views.MapFnOf(v.ж.Map, func(t T) V { return t.View() }) } + +// Unsupported views. func (v GenericCloneableStructView[T, V]) Pointer() map[string]T { panic("unsupported") } func (v GenericCloneableStructView[T, V]) PtrSlice() *T { panic("unsupported") } func (v GenericCloneableStructView[T, V]) PtrKeyMap() map[*T]string { panic("unsupported") } @@ -942,25 +941,21 @@ func (v StructWithTypeAliasFieldsView) SliceWithPtrs() views.SliceView[*StructWi func (v StructWithTypeAliasFieldsView) SliceWithoutPtrs() views.SliceView[*StructWithoutPtrsAlias, StructWithoutPtrsAliasView] { return views.SliceOfViews[*StructWithoutPtrsAlias, StructWithoutPtrsAliasView](v.ж.SliceWithoutPtrs) } - func (v StructWithTypeAliasFieldsView) MapWithPtrs() views.MapFn[string, *StructWithPtrsAlias, StructWithPtrsAliasView] { return views.MapFnOf(v.ж.MapWithPtrs, func(t *StructWithPtrsAlias) StructWithPtrsAliasView { return t.View() }) } - func (v StructWithTypeAliasFieldsView) MapWithoutPtrs() views.MapFn[string, *StructWithoutPtrsAlias, StructWithoutPtrsAliasView] { return views.MapFnOf(v.ж.MapWithoutPtrs, func(t *StructWithoutPtrsAlias) StructWithoutPtrsAliasView { return t.View() }) } - func (v StructWithTypeAliasFieldsView) MapOfSlicesWithPtrs() views.MapFn[string, []*StructWithPtrsAlias, views.SliceView[*StructWithPtrsAlias, StructWithPtrsAliasView]] { return views.MapFnOf(v.ж.MapOfSlicesWithPtrs, func(t []*StructWithPtrsAlias) views.SliceView[*StructWithPtrsAlias, StructWithPtrsAliasView] { return views.SliceOfViews[*StructWithPtrsAlias, StructWithPtrsAliasView](t) }) } - func (v StructWithTypeAliasFieldsView) MapOfSlicesWithoutPtrs() views.MapFn[string, []*StructWithoutPtrsAlias, views.SliceView[*StructWithoutPtrsAlias, StructWithoutPtrsAliasView]] { return views.MapFnOf(v.ж.MapOfSlicesWithoutPtrs, func(t []*StructWithoutPtrsAlias) views.SliceView[*StructWithoutPtrsAlias, StructWithoutPtrsAliasView] { return views.SliceOfViews[*StructWithoutPtrsAlias, StructWithoutPtrsAliasView](t) diff --git a/cmd/viewer/viewer.go b/cmd/viewer/viewer.go index a9617ac10..4fd81ea51 100644 --- a/cmd/viewer/viewer.go +++ b/cmd/viewer/viewer.go @@ -9,6 +9,8 @@ import ( "bytes" "flag" "fmt" + "go/ast" + "go/token" "go/types" "html/template" "log" @@ -17,6 +19,7 @@ import ( "strings" "tailscale.com/util/codegen" + "tailscale.com/util/mak" "tailscale.com/util/must" ) @@ -104,16 +107,13 @@ func (v *{{.ViewName}}{{.TypeParamNames}}) UnmarshalJSONFrom(dec *jsontext.Decod {{define "valuePointerField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.ValuePointer[{{.FieldType}}] { return views.ValuePointerOf(v.ж.{{.FieldName}}) } {{end}} -{{define "mapField"}} -func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})} +{{define "mapField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})} {{end}} -{{define "mapFnField"}} -func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} { +{{define "mapFnField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} { return {{.MapFn}} })} {{end}} -{{define "mapSliceField"}} -func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapSlice[{{.MapKeyType}},{{.MapValueType}}] { return views.MapSliceOf(v.ж.{{.FieldName}}) } +{{define "mapSliceField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapSlice[{{.MapKeyType}},{{.MapValueType}}] { return views.MapSliceOf(v.ж.{{.FieldName}}) } {{end}} {{define "unsupportedField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")} {{end}} @@ -142,7 +142,81 @@ func requiresCloning(t types.Type) (shallow, deep bool, base types.Type) { return p, p, t } -func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ *types.Package) { +type fieldNameKey struct { + typeName string + fieldName string +} + +// getFieldComments extracts field comments from the AST for a given struct type. +func getFieldComments(syntax []*ast.File) map[fieldNameKey]string { + if len(syntax) == 0 { + return nil + } + var fieldComments map[fieldNameKey]string + + // Search through all AST files in the package + for _, file := range syntax { + // Look for the type declaration + for _, decl := range file.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + continue + } + + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + typeName := typeSpec.Name.Name + + // Check if it's a struct type + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + + // Extract field comments + for _, field := range structType.Fields.List { + if len(field.Names) == 0 { + // Anonymous field or no names + continue + } + + // Get the field name + fieldName := field.Names[0].Name + key := fieldNameKey{typeName, fieldName} + + // Get the comment + var comment string + if field.Doc != nil && field.Doc.Text() != "" { + // Format the comment for Go code generation + comment = strings.TrimSpace(field.Doc.Text()) + // Convert multi-line comments to proper Go comment format + var sb strings.Builder + for line := range strings.Lines(comment) { + sb.WriteString("// ") + sb.WriteString(line) + } + if sb.Len() > 0 { + comment = sb.String() + } + } else if field.Comment != nil && field.Comment.Text() != "" { + // Handle inline comments + comment = "// " + strings.TrimSpace(field.Comment.Text()) + } + if comment != "" { + mak.Set(&fieldComments, key, comment) + } + } + } + } + } + + return fieldComments +} + +func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, fieldComments map[fieldNameKey]string) { t, ok := typ.Underlying().(*types.Struct) if !ok || codegen.IsViewType(t) { return @@ -182,6 +256,15 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * log.Fatal(err) } } + writeTemplateWithComment := func(name, fieldName string) { + // Write the field comment if it exists + key := fieldNameKey{args.StructName, fieldName} + if comment, ok := fieldComments[key]; ok && comment != "" { + fmt.Fprintln(buf, comment) + } + writeTemplate(name) + } + writeTemplate("common") for i := range t.NumFields() { f := t.Field(i) @@ -196,7 +279,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * } if !codegen.ContainsPointers(fieldType) || codegen.IsViewType(fieldType) || codegen.HasNoClone(t.Tag(i)) { args.FieldType = it.QualifiedName(fieldType) - writeTemplate("valueField") + writeTemplateWithComment("valueField", fname) continue } switch underlying := fieldType.Underlying().(type) { @@ -207,7 +290,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * case "byte": args.FieldType = it.QualifiedName(fieldType) it.Import("", "tailscale.com/types/views") - writeTemplate("byteSliceField") + writeTemplateWithComment("byteSliceField", fname) default: args.FieldType = it.QualifiedName(elem) it.Import("", "tailscale.com/types/views") @@ -217,35 +300,35 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * case *types.Pointer: if _, isIface := base.Underlying().(*types.Interface); !isIface { args.FieldViewName = appendNameSuffix(it.QualifiedName(base), "View") - writeTemplate("viewSliceField") + writeTemplateWithComment("viewSliceField", fname) } else { - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) } continue case *types.Interface: if viewType := viewTypeForValueType(elem); viewType != nil { args.FieldViewName = it.QualifiedName(viewType) - writeTemplate("viewSliceField") + writeTemplateWithComment("viewSliceField", fname) continue } } - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) continue } else if shallow { switch base.Underlying().(type) { case *types.Basic, *types.Interface: - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) default: if _, isIface := base.Underlying().(*types.Interface); !isIface { args.FieldViewName = appendNameSuffix(it.QualifiedName(base), "View") - writeTemplate("viewSliceField") + writeTemplateWithComment("viewSliceField", fname) } else { - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) } } continue } - writeTemplate("sliceField") + writeTemplateWithComment("sliceField", fname) } continue case *types.Struct: @@ -254,26 +337,26 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * if codegen.ContainsPointers(strucT) { if viewType := viewTypeForValueType(fieldType); viewType != nil { args.FieldViewName = it.QualifiedName(viewType) - writeTemplate("viewField") + writeTemplateWithComment("viewField", fname) continue } if viewType, makeViewFn := viewTypeForContainerType(fieldType); viewType != nil { args.FieldViewName = it.QualifiedName(viewType) args.MakeViewFnName = it.PackagePrefix(makeViewFn.Pkg()) + makeViewFn.Name() - writeTemplate("makeViewField") + writeTemplateWithComment("makeViewField", fname) continue } - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) continue } - writeTemplate("valueField") + writeTemplateWithComment("valueField", fname) continue case *types.Map: m := underlying args.FieldType = it.QualifiedName(fieldType) shallow, deep, key := requiresCloning(m.Key()) if shallow || deep { - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) continue } it.Import("", "tailscale.com/types/views") @@ -358,7 +441,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * default: template = "unsupportedField" } - writeTemplate(template) + writeTemplateWithComment(template, fname) continue case *types.Pointer: ptr := underlying @@ -368,9 +451,9 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * if _, isIface := base.Underlying().(*types.Interface); !isIface { args.FieldType = it.QualifiedName(base) args.FieldViewName = appendNameSuffix(args.FieldType, "View") - writeTemplate("viewField") + writeTemplateWithComment("viewField", fname) } else { - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) } continue } @@ -379,7 +462,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * if viewType := viewTypeForValueType(base); viewType != nil { args.FieldType = it.QualifiedName(base) args.FieldViewName = it.QualifiedName(viewType) - writeTemplate("viewField") + writeTemplateWithComment("viewField", fname) continue } @@ -389,7 +472,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * baseTypeName := it.QualifiedName(base) args.FieldType = baseTypeName args.FieldViewName = appendNameSuffix(args.FieldType, "View") - writeTemplate("viewField") + writeTemplateWithComment("viewField", fname) continue } @@ -397,18 +480,18 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, _ * // and will not have a generated view type, use views.ValuePointer[T] as the field's view type. // Its Get/GetOk methods return stack-allocated shallow copies of the field's value. args.FieldType = it.QualifiedName(base) - writeTemplate("valuePointerField") + writeTemplateWithComment("valuePointerField", fname) continue case *types.Interface: // If fieldType is an interface with a "View() {ViewType}" method, it can be used to clone the field. // This includes scenarios where fieldType is a constrained type parameter. if viewType := viewTypeForValueType(underlying); viewType != nil { args.FieldViewName = it.QualifiedName(viewType) - writeTemplate("viewField") + writeTemplateWithComment("viewField", fname) continue } } - writeTemplate("unsupportedField") + writeTemplateWithComment("unsupportedField", fname) } for i := range typ.NumMethods() { f := typ.Method(i) @@ -627,6 +710,7 @@ func main() { log.Fatal(err) } it := codegen.NewImportTracker(pkg.Types) + fieldComments := getFieldComments(pkg.Syntax) cloneOnlyType := map[string]bool{} for _, t := range strings.Split(*flagCloneOnlyTypes, ",") { @@ -654,7 +738,7 @@ func main() { if !hasClone { runCloner = true } - genView(buf, it, typ, pkg.Types) + genView(buf, it, typ, fieldComments) } out := pkg.Name + "_view" if *flagBuildTags == "test" { diff --git a/cmd/viewer/viewer_test.go b/cmd/viewer/viewer_test.go index d12d49655..1e24b7050 100644 --- a/cmd/viewer/viewer_test.go +++ b/cmd/viewer/viewer_test.go @@ -53,6 +53,7 @@ func TestViewerImports(t *testing.T) { if err != nil { t.Fatal(err) } + var fieldComments map[fieldNameKey]string // don't need it for this test. var output bytes.Buffer tracker := codegen.NewImportTracker(pkg) @@ -65,7 +66,7 @@ func TestViewerImports(t *testing.T) { if !ok { t.Fatalf("%q is not a named type", tt.typeNames[i]) } - genView(&output, tracker, namedType, pkg) + genView(&output, tracker, namedType, fieldComments) } for _, pkg := range tt.wantImports { diff --git a/drive/drive_view.go b/drive/drive_view.go index 6338705a6..b481751bb 100644 --- a/drive/drive_view.go +++ b/drive/drive_view.go @@ -83,9 +83,24 @@ func (v *ShareView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// Name is how this share appears on remote nodes. func (v ShareView) Name() string { return v.ж.Name } + +// Path is the path to the directory on this machine that's being shared. func (v ShareView) Path() string { return v.ж.Path } -func (v ShareView) As() string { return v.ж.As } + +// As is the UNIX or Windows username of the local account used for this +// share. File read/write permissions are enforced based on this username. +// Can be left blank to use the default value of "whoever is running the +// Tailscale GUI". +func (v ShareView) As() string { return v.ж.As } + +// BookmarkData contains security-scoped bookmark data for the Sandboxed +// Mac application. The Sandboxed Mac application gains permission to +// access the Share's folder as a result of a user selecting it in a file +// picker. In order to retain access to it across restarts, it needs to +// hold on to a security-scoped bookmark. That bookmark is stored here. See +// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox#4144043 func (v ShareView) BookmarkData() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.BookmarkData) } diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 0f0f652d1..170dc409b 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -89,14 +89,47 @@ func (v *LoginProfileView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v LoginProfileView) ID() ProfileID { return v.ж.ID } -func (v LoginProfileView) Name() string { return v.ж.Name } -func (v LoginProfileView) NetworkProfile() NetworkProfile { return v.ж.NetworkProfile } -func (v LoginProfileView) Key() StateKey { return v.ж.Key } +// ID is a unique identifier for this profile. +// It is assigned on creation and never changes. +// It may seem redundant to have both ID and UserProfile.ID +// but they are different things. UserProfile.ID may change +// over time (e.g. if a device is tagged). +func (v LoginProfileView) ID() ProfileID { return v.ж.ID } + +// Name is the user-visible name of this profile. +// It is filled in from the UserProfile.LoginName field. +func (v LoginProfileView) Name() string { return v.ж.Name } + +// NetworkProfile is a subset of netmap.NetworkMap that we +// store to remember information about the tailnet that this +// profile was logged in with. +// +// This field was added on 2023-11-17. +func (v LoginProfileView) NetworkProfile() NetworkProfile { return v.ж.NetworkProfile } + +// Key is the StateKey under which the profile is stored. +// It is assigned once at profile creation time and never changes. +func (v LoginProfileView) Key() StateKey { return v.ж.Key } + +// UserProfile is the server provided UserProfile for this profile. +// This is updated whenever the server provides a new UserProfile. func (v LoginProfileView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile } -func (v LoginProfileView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID } -func (v LoginProfileView) LocalUserID() WindowsUserID { return v.ж.LocalUserID } -func (v LoginProfileView) ControlURL() string { return v.ж.ControlURL } + +// NodeID is the NodeID of the node that this profile is logged into. +// This should be stable across tagging and untagging nodes. +// It may seem redundant to check against both the UserProfile.UserID +// and the NodeID. However the NodeID can change if the node is deleted +// from the admin panel. +func (v LoginProfileView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID } + +// LocalUserID is the user ID of the user who created this profile. +// It is only relevant on Windows where we have a multi-user system. +// It is assigned once at profile creation time and never changes. +func (v LoginProfileView) LocalUserID() WindowsUserID { return v.ж.LocalUserID } + +// ControlURL is the URL of the control server that this profile is logged +// into. +func (v LoginProfileView) ControlURL() string { return v.ж.ControlURL } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _LoginProfileViewNeedsRegeneration = LoginProfile(struct { @@ -177,48 +210,253 @@ func (v *PrefsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v PrefsView) ControlURL() string { return v.ж.ControlURL } -func (v PrefsView) RouteAll() bool { return v.ж.RouteAll } -func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID } -func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP } -func (v PrefsView) AutoExitNode() ExitNodeExpression { return v.ж.AutoExitNode } +// ControlURL is the URL of the control server to use. +// +// If empty, the default for new installs, DefaultControlURL +// is used. It's set non-empty once the daemon has been started +// for the first time. +// +// TODO(apenwarr): Make it safe to update this with EditPrefs(). +// Right now, you have to pass it in the initial prefs in Start(), +// which is the only code that actually uses the ControlURL value. +// It would be more consistent to restart controlclient +// automatically whenever this variable changes. +// +// Meanwhile, you have to provide this as part of +// Options.LegacyMigrationPrefs or Options.UpdatePrefs when +// calling Backend.Start(). +func (v PrefsView) ControlURL() string { return v.ж.ControlURL } + +// RouteAll specifies whether to accept subnets advertised by +// other nodes on the Tailscale network. Note that this does not +// include default routes (0.0.0.0/0 and ::/0), those are +// controlled by ExitNodeID/IP below. +func (v PrefsView) RouteAll() bool { return v.ж.RouteAll } + +// ExitNodeID and ExitNodeIP specify the node that should be used +// as an exit node for internet traffic. At most one of these +// should be non-zero. +// +// The preferred way to express the chosen node is ExitNodeID, but +// in some cases it's not possible to use that ID (e.g. in the +// linux CLI, before tailscaled has a netmap). For those +// situations, we allow specifying the exit node by IP, and +// ipnlocal.LocalBackend will translate the IP into an ID when the +// node is found in the netmap. +// +// If the selected exit node doesn't exist (e.g. it's not part of +// the current tailnet), or it doesn't offer exit node services, a +// blackhole route will be installed on the local system to +// prevent any traffic escaping to the local network. +func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID } +func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP } + +// AutoExitNode is an optional expression that specifies whether and how +// tailscaled should pick an exit node automatically. +// +// If specified, tailscaled will use an exit node based on the expression, +// and will re-evaluate the selection periodically as network conditions, +// available exit nodes, or policy settings change. A blackhole route will +// be installed to prevent traffic from escaping to the local network until +// an exit node is selected. It takes precedence over ExitNodeID and ExitNodeIP. +// +// If empty, tailscaled will not automatically select an exit node. +// +// If the specified expression is invalid or unsupported by the client, +// it falls back to the behavior of [AnyExitNode]. +// +// As of 2025-07-02, the only supported value is [AnyExitNode]. +// It's a string rather than a boolean to allow future extensibility +// (e.g., AutoExitNode = "mullvad" or AutoExitNode = "geo:us"). +func (v PrefsView) AutoExitNode() ExitNodeExpression { return v.ж.AutoExitNode } + +// InternalExitNodePrior is the most recently used ExitNodeID in string form. It is set by +// the backend on transition from exit node on to off and used by the +// backend. +// +// As an Internal field, it can't be set by LocalAPI clients, rather it is set indirectly +// when the ExitNodeID value is zero'd and via the set-use-exit-node-enabled endpoint. func (v PrefsView) InternalExitNodePrior() tailcfg.StableNodeID { return v.ж.InternalExitNodePrior } -func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess } -func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS } -func (v PrefsView) RunSSH() bool { return v.ж.RunSSH } -func (v PrefsView) RunWebClient() bool { return v.ж.RunWebClient } -func (v PrefsView) WantRunning() bool { return v.ж.WantRunning } -func (v PrefsView) LoggedOut() bool { return v.ж.LoggedOut } -func (v PrefsView) ShieldsUp() bool { return v.ж.ShieldsUp } -func (v PrefsView) AdvertiseTags() views.Slice[string] { return views.SliceOf(v.ж.AdvertiseTags) } -func (v PrefsView) Hostname() string { return v.ж.Hostname } -func (v PrefsView) NotepadURLs() bool { return v.ж.NotepadURLs } -func (v PrefsView) ForceDaemon() bool { return v.ж.ForceDaemon } -func (v PrefsView) Egg() bool { return v.ж.Egg } + +// ExitNodeAllowLANAccess indicates whether locally accessible subnets should be +// routed directly or via the exit node. +func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess } + +// CorpDNS specifies whether to install the Tailscale network's +// DNS configuration, if it exists. +func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS } + +// RunSSH bool is whether this node should run an SSH +// server, permitting access to peers according to the +// policies as configured by the Tailnet's admin(s). +func (v PrefsView) RunSSH() bool { return v.ж.RunSSH } + +// RunWebClient bool is whether this node should expose +// its web client over Tailscale at port 5252, +// permitting access to peers according to the +// policies as configured by the Tailnet's admin(s). +func (v PrefsView) RunWebClient() bool { return v.ж.RunWebClient } + +// WantRunning indicates whether networking should be active on +// this node. +func (v PrefsView) WantRunning() bool { return v.ж.WantRunning } + +// LoggedOut indicates whether the user intends to be logged out. +// There are other reasons we may be logged out, including no valid +// keys. +// We need to remember this state so that, on next startup, we can +// generate the "Login" vs "Connect" buttons correctly, without having +// to contact the server to confirm our nodekey status first. +func (v PrefsView) LoggedOut() bool { return v.ж.LoggedOut } + +// ShieldsUp indicates whether to block all incoming connections, +// regardless of the control-provided packet filter. If false, we +// use the packet filter as provided. If true, we block incoming +// connections. This overrides tailcfg.Hostinfo's ShieldsUp. +func (v PrefsView) ShieldsUp() bool { return v.ж.ShieldsUp } + +// AdvertiseTags specifies tags that should be applied to this node, for +// purposes of ACL enforcement. These can be referenced from the ACL policy +// document. Note that advertising a tag on the client doesn't guarantee +// that the control server will allow the node to adopt that tag. +func (v PrefsView) AdvertiseTags() views.Slice[string] { return views.SliceOf(v.ж.AdvertiseTags) } + +// Hostname is the hostname to use for identifying the node. If +// not set, os.Hostname is used. +func (v PrefsView) Hostname() string { return v.ж.Hostname } + +// NotepadURLs is a debugging setting that opens OAuth URLs in +// notepad.exe on Windows, rather than loading them in a browser. +// +// apenwarr 2020-04-29: Unfortunately this is still needed sometimes. +// Windows' default browser setting is sometimes screwy and this helps +// users narrow it down a bit. +func (v PrefsView) NotepadURLs() bool { return v.ж.NotepadURLs } + +// ForceDaemon specifies whether a platform that normally +// operates in "client mode" (that is, requires an active user +// logged in with the GUI app running) should keep running after the +// GUI ends and/or the user logs out. +// +// The only current applicable platform is Windows. This +// forced Windows to go into "server mode" where Tailscale is +// running even with no users logged in. This might also be +// used for macOS in the future. This setting has no effect +// for Linux/etc, which always operate in daemon mode. +func (v PrefsView) ForceDaemon() bool { return v.ж.ForceDaemon } + +// Egg is a optional debug flag. +func (v PrefsView) Egg() bool { return v.ж.Egg } + +// AdvertiseRoutes specifies CIDR prefixes to advertise into the +// Tailscale network as reachable through the current +// node. func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AdvertiseRoutes) } + +// AdvertiseServices specifies the list of services that this +// node can serve as a destination for. Note that an advertised +// service must still go through the approval process from the +// control server. func (v PrefsView) AdvertiseServices() views.Slice[string] { return views.SliceOf(v.ж.AdvertiseServices) } -func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT } -func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering } + +// NoSNAT specifies whether to source NAT traffic going to +// destinations in AdvertiseRoutes. The default is to apply source +// NAT, which makes the traffic appear to come from the router +// machine rather than the peer's Tailscale IP. +// +// Disabling SNAT requires additional manual configuration in your +// network to route Tailscale traffic back to the subnet relay +// machine. +// +// Linux-only. +func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT } + +// NoStatefulFiltering specifies whether to apply stateful filtering when +// advertising routes in AdvertiseRoutes. The default is to not apply +// stateful filtering. +// +// To allow inbound connections from advertised routes, both NoSNAT and +// NoStatefulFiltering must be true. +// +// This is an opt.Bool because it was first added after NoSNAT, with a +// backfill based on the value of that parameter. The backfill has been +// removed since then, but the field remains an opt.Bool. +// +// Linux-only. +func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering } + +// NetfilterMode specifies how much to manage netfilter rules for +// Tailscale, if at all. func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode } -func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser } -func (v PrefsView) ProfileName() string { return v.ж.ProfileName } -func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate } -func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector } -func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking } -func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind } + +// OperatorUser is the local machine user name who is allowed to +// operate tailscaled without being root or using sudo. +func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser } + +// ProfileName is the desired name of the profile. If empty, then the user's +// LoginName is used. It is only used for display purposes in the client UI +// and CLI. +func (v PrefsView) ProfileName() string { return v.ж.ProfileName } + +// AutoUpdate sets the auto-update preferences for the node agent. See +// AutoUpdatePrefs docs for more details. +func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate } + +// AppConnector sets the app connector preferences for the node agent. See +// AppConnectorPrefs docs for more details. +func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector } + +// PostureChecking enables the collection of information used for device +// posture checks. +// +// Note: this should be named ReportPosture, but it was shipped as +// PostureChecking in some early releases and this JSON field is written to +// disk, so we just keep its old name. (akin to CorpDNS which is an internal +// pref name that doesn't match the public interface) +func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking } + +// NetfilterKind specifies what netfilter implementation to use. +// +// Linux-only. +func (v PrefsView) NetfilterKind() string { return v.ж.NetfilterKind } + +// DriveShares are the configured DriveShares, stored in increasing order +// by name. func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] { return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares) } + +// RelayServerPort is the UDP port number for the relay server to bind to, +// on all interfaces. A non-nil zero value signifies a random unused port +// should be used. A nil value signifies relay server functionality +// should be disabled. This field is currently experimental, and therefore +// no guarantees are made about its current naming and functionality when +// non-nil/enabled. func (v PrefsView) RelayServerPort() views.ValuePointer[int] { return views.ValuePointerOf(v.ж.RelayServerPort) } +// AllowSingleHosts was a legacy field that was always true +// for the past 4.5 years. It controlled whether Tailscale +// peers got /32 or /127 routes for each other. +// As of 2024-05-17 we're starting to ignore it, but to let +// people still downgrade Tailscale versions and not break +// all peer-to-peer networking we still write it to disk (as JSON) +// so it can be loaded back by old versions. +// TODO(bradfitz): delete this in 2025 sometime. See #12058. func (v PrefsView) AllowSingleHosts() marshalAsTrueInJSON { return v.ж.AllowSingleHosts } -func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } + +// The Persist field is named 'Config' in the file for backward +// compatibility with earlier versions. +// TODO(apenwarr): We should move this out of here, it's not a pref. +// +// We can maybe do that once we're sure which module should persist +// it (backend or frontend?) +func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _PrefsViewNeedsRegeneration = Prefs(struct { @@ -324,33 +562,52 @@ func (v *ServeConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// TCP are the list of TCP port numbers that tailscaled should handle for +// the Tailscale IP addresses. (not subnet routers, etc) func (v ServeConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandlerView] { return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView { return t.View() }) } +// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers +// keyed by mount point ("/", "/foo", etc) func (v ServeConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServerConfigView] { return views.MapFnOf(v.ж.Web, func(t *WebServerConfig) WebServerConfigView { return t.View() }) } +// Services maps from service name (in the form "svc:dns-label") to a ServiceConfig. +// Which describes the L3, L4, and L7 forwarding information for the service. func (v ServeConfigView) Services() views.MapFn[tailcfg.ServiceName, *ServiceConfig, ServiceConfigView] { return views.MapFnOf(v.ж.Services, func(t *ServiceConfig) ServiceConfigView { return t.View() }) } +// AllowFunnel is the set of SNI:port values for which funnel +// traffic is allowed, from trusted ingress peers. func (v ServeConfigView) AllowFunnel() views.Map[HostPort, bool] { return views.MapOf(v.ж.AllowFunnel) } +// Foreground is a map of an IPN Bus session ID to an alternate foreground serve config that's valid for the +// life of that WatchIPNBus session ID. This allows the config to specify ephemeral configs that are used +// in the CLI's foreground mode to ensure ungraceful shutdowns of either the client or the LocalBackend does not +// expose ports that users are not aware of. In practice this contains any serve config set via 'tailscale +// serve' command run without the '--bg' flag. ServeConfig contained by Foreground is not expected itself to contain +// another Foreground block. func (v ServeConfigView) Foreground() views.MapFn[string, *ServeConfig, ServeConfigView] { return views.MapFnOf(v.ж.Foreground, func(t *ServeConfig) ServeConfigView { return t.View() }) } + +// ETag is the checksum of the serve config that's populated +// by the LocalClient through the HTTP ETag header during a +// GetServeConfig request and is translated to an If-Match header +// during a SetServeConfig request. func (v ServeConfigView) ETag() string { return v.ж.ETag } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -430,17 +687,23 @@ func (v *ServiceConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// TCP are the list of TCP port numbers that tailscaled should handle for +// the Tailscale IP addresses. (not subnet routers, etc) func (v ServiceConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandlerView] { return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView { return t.View() }) } +// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers +// keyed by mount point ("/", "/foo", etc) func (v ServiceConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServerConfigView] { return views.MapFnOf(v.ж.Web, func(t *WebServerConfig) WebServerConfigView { return t.View() }) } + +// Tun determines if the service should be using L3 forwarding (Tun mode). func (v ServiceConfigView) Tun() bool { return v.ж.Tun } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -517,9 +780,29 @@ func (v *TCPPortHandlerView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v TCPPortHandlerView) HTTPS() bool { return v.ж.HTTPS } -func (v TCPPortHandlerView) HTTP() bool { return v.ж.HTTP } -func (v TCPPortHandlerView) TCPForward() string { return v.ж.TCPForward } +// HTTPS, if true, means that tailscaled should handle this connection as an +// HTTPS request as configured by ServeConfig.Web. +// +// It is mutually exclusive with TCPForward. +func (v TCPPortHandlerView) HTTPS() bool { return v.ж.HTTPS } + +// HTTP, if true, means that tailscaled should handle this connection as an +// HTTP request as configured by ServeConfig.Web. +// +// It is mutually exclusive with TCPForward. +func (v TCPPortHandlerView) HTTP() bool { return v.ж.HTTP } + +// TCPForward is the IP:port to forward TCP connections to. +// Whether or not TLS is terminated by tailscaled depends on +// TerminateTLS. +// +// It is mutually exclusive with HTTPS. +func (v TCPPortHandlerView) TCPForward() string { return v.ж.TCPForward } + +// TerminateTLS, if non-empty, means that tailscaled should terminate the +// TLS connections before forwarding them to TCPForward, permitting only the +// SNI name with this value. It is only used if TCPForward is non-empty. +// (the HTTPS mode uses ServeConfig.Web) func (v TCPPortHandlerView) TerminateTLS() string { return v.ж.TerminateTLS } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -597,9 +880,14 @@ func (v *HTTPHandlerView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v HTTPHandlerView) Path() string { return v.ж.Path } +// absolute path to directory or file to serve +func (v HTTPHandlerView) Path() string { return v.ж.Path } + +// http://localhost:3000/, localhost:3030, 3030 func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy } -func (v HTTPHandlerView) Text() string { return v.ж.Text } + +// plaintext to serve (primarily for testing) +func (v HTTPHandlerView) Text() string { return v.ж.Text } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct { @@ -675,6 +963,7 @@ func (v *WebServerConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// mountPoint => handler func (v WebServerConfigView) Handlers() views.MapFn[string, *HTTPHandler, HTTPHandlerView] { return views.MapFnOf(v.ж.Handlers, func(t *HTTPHandler) HTTPHandlerView { return t.View() diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 8dc4f1ca8..e44d0bbef 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -90,8 +90,12 @@ func (v *UserView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v UserView) ID() UserID { return v.ж.ID } -func (v UserView) DisplayName() string { return v.ж.DisplayName } +func (v UserView) ID() UserID { return v.ж.ID } + +// if non-empty overrides Login field +func (v UserView) DisplayName() string { return v.ж.DisplayName } + +// if non-empty overrides Login field func (v UserView) ProfilePicURL() string { return v.ж.ProfilePicURL } func (v UserView) Created() time.Time { return v.ж.Created } @@ -172,53 +176,202 @@ func (v *NodeView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { 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 } + +// Name is the FQDN of the node. +// It is also the MagicDNS name for the node. +// It has a trailing dot. +// e.g. "host.tail-scale.ts.net." +func (v NodeView) Name() string { return v.ж.Name } + +// User is the user who created the node. If ACL tags are in use for the +// node then it doesn't reflect the ACL identity that the node is running +// as. +func (v NodeView) User() UserID { return v.ж.User } + +// Sharer, if non-zero, is the user who shared this node, if different than User. +func (v NodeView) Sharer() UserID { return v.ж.Sharer } +func (v NodeView) Key() key.NodePublic { return v.ж.Key } + +// the zero value if this node does not expire +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) } -func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) } -func (v NodeView) Endpoints() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Endpoints) } -func (v NodeView) LegacyDERPString() string { return v.ж.LegacyDERPString } -func (v NodeView) HomeDERP() int { return v.ж.HomeDERP } -func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo } -func (v NodeView) Created() time.Time { return v.ж.Created } -func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap } -func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) } +func (v NodeView) Machine() key.MachinePublic { return v.ж.Machine } +func (v NodeView) DiscoKey() key.DiscoPublic { return v.ж.DiscoKey } + +// Addresses are the IP addresses of this Node directly. +func (v NodeView) Addresses() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.Addresses) } + +// AllowedIPs are the IP ranges to route to this node. +// +// As of CapabilityVersion 112, this may be nil (null or undefined) on the wire +// to mean the same as Addresses. Internally, it is always filled in with +// its possibly-implicit value. +func (v NodeView) AllowedIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.AllowedIPs) } + +// IP+port (public via STUN, and local LANs) +func (v NodeView) Endpoints() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Endpoints) } + +// LegacyDERPString is this node's home LegacyDERPString region ID integer, but shoved into an +// IP:port string for legacy reasons. The IP address is always "127.3.3.40" +// (a loopback address (127) followed by the digits over the letters DERP on +// a QWERTY keyboard (3.3.40)). The "port number" is the home LegacyDERPString region ID +// integer. +// +// Deprecated: HomeDERP has replaced this, but old servers might still send +// this field. See tailscale/tailscale#14636. Do not use this field in code +// other than in the upgradeNode func, which canonicalizes it to HomeDERP +// if it arrives as a LegacyDERPString string on the wire. +func (v NodeView) LegacyDERPString() string { return v.ж.LegacyDERPString } + +// HomeDERP is the modern version of the DERP string field, with just an +// integer. The client advertises support for this as of capver 111. +// +// HomeDERP may be zero if not (yet) known, but ideally always be non-zero +// for magicsock connectivity to function normally. +func (v NodeView) HomeDERP() int { return v.ж.HomeDERP } +func (v NodeView) Hostinfo() HostinfoView { return v.ж.Hostinfo } +func (v NodeView) Created() time.Time { return v.ж.Created } + +// if non-zero, the node's capability version; old servers might not send +func (v NodeView) Cap() CapabilityVersion { return v.ж.Cap } + +// Tags are the list of ACL tags applied to this node. +// Tags take the form of `tag:` where value starts +// with a letter and only contains alphanumerics and dashes `-`. +// Some valid tag examples: +// +// `tag:prod` +// `tag:database` +// `tag:lab-1` +func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) } + +// PrimaryRoutes are the routes from AllowedIPs that this node +// is currently the primary subnet router for, as determined +// by the control plane. It does not include the self address +// values from Addresses that are in AllowedIPs. func (v NodeView) PrimaryRoutes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.PrimaryRoutes) } + +// LastSeen is when the node was last online. It is not +// updated when Online is true. It is nil if the current +// node doesn't have permission to know, or the node +// has never been online. func (v NodeView) LastSeen() views.ValuePointer[time.Time] { return views.ValuePointerOf(v.ж.LastSeen) } +// Online is whether the node is currently connected to the +// coordination server. A value of nil means unknown, or the +// current node doesn't have permission to know. func (v NodeView) Online() views.ValuePointer[bool] { return views.ValuePointerOf(v.ж.Online) } -func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } +// TODO(crawshaw): replace with MachineStatus +func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } + +// Capabilities are capabilities that the node has. +// They're free-form strings, but should be in the form of URLs/URIs +// such as: +// +// "https://tailscale.com/cap/is-admin" +// "https://tailscale.com/cap/file-sharing" +// +// Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508 func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.SliceOf(v.ж.Capabilities) } +// CapMap is a map of capabilities to their optional argument/data values. +// +// It is valid for a capability to not have any argument/data values; such +// capabilities can be tested for using the HasCap method. These type of +// capabilities are used to indicate that a node has a capability, but there +// is no additional data associated with it. These were previously +// represented by the Capabilities field, but can now be represented by +// CapMap with an empty value. +// +// See NodeCapability for more information on keys. +// +// Metadata about nodes can be transmitted in 3 ways: +// 1. MapResponse.Node.CapMap describes attributes that affect behavior for +// this node, such as which features have been enabled through the admin +// panel and any associated configuration details. +// 2. MapResponse.PacketFilter(s) describes access (both IP and application +// based) that should be granted to peers. +// 3. MapResponse.Peers[].CapMap describes attributes regarding a peer node, +// such as which features the peer supports or if that peer is preferred +// for a particular task vs other peers that could also be chosen. func (v NodeView) CapMap() views.MapSlice[NodeCapability, RawMessage] { return views.MapSliceOf(v.ж.CapMap) } -func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly } -func (v NodeView) ComputedName() string { return v.ж.ComputedName } + +// UnsignedPeerAPIOnly means that this node is not signed nor subject to TKA +// restrictions. However, in exchange for that privilege, it does not get +// network access. It can only access this node's peerapi, which may not let +// it do anything. It is the tailscaled client's job to double-check the +// MapResponse's PacketFilter to verify that its AllowedIPs will not be +// accepted by the packet filter. +func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly } + +// MagicDNS base name (for normal non-shared-in nodes), FQDN (without trailing dot, for shared-in nodes), or Hostname (if no MagicDNS) +func (v NodeView) ComputedName() string { return v.ж.ComputedName } + +// either "ComputedName" or "ComputedName (computedHostIfDifferent)", if computedHostIfDifferent is set func (v NodeView) ComputedNameWithHost() string { return v.ж.ComputedNameWithHost } -func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID } -func (v NodeView) Expired() bool { return v.ж.Expired } + +// DataPlaneAuditLogID is the per-node logtail ID used for data plane audit logging. +func (v NodeView) DataPlaneAuditLogID() string { return v.ж.DataPlaneAuditLogID } + +// Expired is whether this node's key has expired. Control may send +// this; clients are only allowed to set this from false to true. On +// the client, this is calculated client-side based on a timestamp sent +// from control, to avoid clock skew issues. +func (v NodeView) Expired() bool { return v.ж.Expired } + +// SelfNodeV4MasqAddrForThisPeer is the IPv4 that this peer knows the current node as. +// It may be empty if the peer knows the current node by its native +// IPv4 address. +// This field is only populated in a MapResponse for peers and not +// for the current node. +// +// If set, it should be used to masquerade traffic originating from the +// current node to this peer. The masquerade address is only relevant +// for this peer and not for other peers. +// +// This only applies to traffic originating from the current node to the +// peer or any of its subnets. Traffic originating from subnet routes will +// not be masqueraded (e.g. in case of --snat-subnet-routes). func (v NodeView) SelfNodeV4MasqAddrForThisPeer() views.ValuePointer[netip.Addr] { return views.ValuePointerOf(v.ж.SelfNodeV4MasqAddrForThisPeer) } +// SelfNodeV6MasqAddrForThisPeer is the IPv6 that this peer knows the current node as. +// It may be empty if the peer knows the current node by its native +// IPv6 address. +// This field is only populated in a MapResponse for peers and not +// for the current node. +// +// If set, it should be used to masquerade traffic originating from the +// current node to this peer. The masquerade address is only relevant +// for this peer and not for other peers. +// +// This only applies to traffic originating from the current node to the +// peer or any of its subnets. Traffic originating from subnet routes will +// not be masqueraded (e.g. in case of --snat-subnet-routes). func (v NodeView) SelfNodeV6MasqAddrForThisPeer() views.ValuePointer[netip.Addr] { return views.ValuePointerOf(v.ж.SelfNodeV6MasqAddrForThisPeer) } +// IsWireGuardOnly indicates that this is a non-Tailscale WireGuard peer, it +// is not expected to speak Disco or DERP, and it must have Endpoints in +// order to be reachable. func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly } -func (v NodeView) IsJailed() bool { return v.ж.IsJailed } + +// IsJailed indicates that this node is jailed and should not be allowed +// initiate connections, however outbound connections to it should still be +// allowed. +func (v NodeView) IsJailed() bool { return v.ж.IsJailed } + +// ExitNodeDNSResolvers is the list of DNS servers that should be used when this +// node is marked IsWireGuardOnly and being used as an exit node. func (v NodeView) ExitNodeDNSResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.ExitNodeDNSResolvers) } @@ -331,47 +484,144 @@ func (v *HostinfoView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion } -func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID } -func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID } -func (v HostinfoView) OS() string { return v.ж.OS } -func (v HostinfoView) OSVersion() string { return v.ж.OSVersion } -func (v HostinfoView) Container() opt.Bool { return v.ж.Container } -func (v HostinfoView) Env() string { return v.ж.Env } -func (v HostinfoView) Distro() string { return v.ж.Distro } -func (v HostinfoView) DistroVersion() string { return v.ж.DistroVersion } -func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName } -func (v HostinfoView) App() string { return v.ж.App } -func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop } -func (v HostinfoView) Package() string { return v.ж.Package } -func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel } -func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken } -func (v HostinfoView) Hostname() string { return v.ж.Hostname } -func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp } -func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode } -func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport } -func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress } -func (v HostinfoView) IngressEnabled() bool { return v.ж.IngressEnabled } -func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate } -func (v HostinfoView) Machine() string { return v.ж.Machine } -func (v HostinfoView) GoArch() string { return v.ж.GoArch } -func (v HostinfoView) GoArchVar() string { return v.ж.GoArchVar } -func (v HostinfoView) GoVersion() string { return v.ж.GoVersion } +// version of this code (in version.Long format) +func (v HostinfoView) IPNVersion() string { return v.ж.IPNVersion } + +// logtail ID of frontend instance +func (v HostinfoView) FrontendLogID() string { return v.ж.FrontendLogID } + +// logtail ID of backend instance +func (v HostinfoView) BackendLogID() string { return v.ж.BackendLogID } + +// operating system the client runs on (a version.OS value) +func (v HostinfoView) OS() string { return v.ж.OS } + +// OSVersion is the version of the OS, if available. +// +// For Android, it's like "10", "11", "12", etc. For iOS and macOS it's like +// "15.6.1" or "12.4.0". For Windows it's like "10.0.19044.1889". For +// FreeBSD it's like "12.3-STABLE". +// +// For Linux, prior to Tailscale 1.32, we jammed a bunch of fields into this +// string on Linux, like "Debian 10.4; kernel=xxx; container; env=kn" and so +// on. As of Tailscale 1.32, this is simply the kernel version on Linux, like +// "5.10.0-17-amd64". +func (v HostinfoView) OSVersion() string { return v.ж.OSVersion } + +// best-effort whether the client is running in a container +func (v HostinfoView) Container() opt.Bool { return v.ж.Container } + +// a hostinfo.EnvType in string form +func (v HostinfoView) Env() string { return v.ж.Env } + +// "debian", "ubuntu", "nixos", ... +func (v HostinfoView) Distro() string { return v.ж.Distro } + +// "20.04", ... +func (v HostinfoView) DistroVersion() string { return v.ж.DistroVersion } + +// "jammy", "bullseye", ... +func (v HostinfoView) DistroCodeName() string { return v.ж.DistroCodeName } + +// App is used to disambiguate Tailscale clients that run using tsnet. +func (v HostinfoView) App() string { return v.ж.App } + +// if a desktop was detected on Linux +func (v HostinfoView) Desktop() opt.Bool { return v.ж.Desktop } + +// Tailscale package to disambiguate ("choco", "appstore", etc; "" for unknown) +func (v HostinfoView) Package() string { return v.ж.Package } + +// mobile phone model ("Pixel 3a", "iPhone12,3") +func (v HostinfoView) DeviceModel() string { return v.ж.DeviceModel } + +// macOS/iOS APNs device token for notifications (and Android in the future) +func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken } + +// name of the host the client runs on +func (v HostinfoView) Hostname() string { return v.ж.Hostname } + +// indicates whether the host is blocking incoming connections +func (v HostinfoView) ShieldsUp() bool { return v.ж.ShieldsUp } + +// indicates this node exists in netmap because it's owned by a shared-to user +func (v HostinfoView) ShareeNode() bool { return v.ж.ShareeNode } + +// indicates that the user has opted out of sending logs and support +func (v HostinfoView) NoLogsNoSupport() bool { return v.ж.NoLogsNoSupport } + +// WireIngress indicates that the node would like to be wired up server-side +// (DNS, etc) to be able to use Tailscale Funnel, even if it's not currently +// enabled. For example, the user might only use it for intermittent +// foreground CLI serve sessions, for which they'd like it to work right +// away, even if it's disabled most of the time. As an optimization, this is +// only sent if IngressEnabled is false, as IngressEnabled implies that this +// option is true. +func (v HostinfoView) WireIngress() bool { return v.ж.WireIngress } + +// if the node has any funnel endpoint enabled +func (v HostinfoView) IngressEnabled() bool { return v.ж.IngressEnabled } + +// indicates that the node has opted-in to admin-console-drive remote updates +func (v HostinfoView) AllowsUpdate() bool { return v.ж.AllowsUpdate } + +// the current host's machine type (uname -m) +func (v HostinfoView) Machine() string { return v.ж.Machine } + +// GOARCH value (of the built binary) +func (v HostinfoView) GoArch() string { return v.ж.GoArch } + +// GOARM, GOAMD64, etc (of the built binary) +func (v HostinfoView) GoArchVar() string { return v.ж.GoArchVar } + +// Go version binary was built with +func (v HostinfoView) GoVersion() string { return v.ж.GoVersion } + +// set of IP ranges this client can route func (v HostinfoView) RoutableIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.RoutableIPs) } -func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) } -func (v HostinfoView) WoLMACs() views.Slice[string] { return views.SliceOf(v.ж.WoLMACs) } -func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) } -func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() } -func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) } -func (v HostinfoView) Cloud() string { return v.ж.Cloud } -func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace } -func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter } -func (v HostinfoView) AppConnector() opt.Bool { return v.ж.AppConnector } -func (v HostinfoView) ServicesHash() string { return v.ж.ServicesHash } -func (v HostinfoView) ExitNodeID() StableNodeID { return v.ж.ExitNodeID } -func (v HostinfoView) Location() LocationView { return v.ж.Location.View() } -func (v HostinfoView) TPM() views.ValuePointer[TPMInfo] { return views.ValuePointerOf(v.ж.TPM) } +// set of ACL tags this node wants to claim +func (v HostinfoView) RequestTags() views.Slice[string] { return views.SliceOf(v.ж.RequestTags) } + +// MAC address(es) to send Wake-on-LAN packets to wake this node (lowercase hex w/ colons) +func (v HostinfoView) WoLMACs() views.Slice[string] { return views.SliceOf(v.ж.WoLMACs) } + +// services advertised by this machine +func (v HostinfoView) Services() views.Slice[Service] { return views.SliceOf(v.ж.Services) } +func (v HostinfoView) NetInfo() NetInfoView { return v.ж.NetInfo.View() } + +// if advertised +func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) } +func (v HostinfoView) Cloud() string { return v.ж.Cloud } + +// if the client is running in userspace (netstack) mode +func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace } + +// if the client's subnet router is running in userspace (netstack) mode +func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter } + +// if the client is running the app-connector service +func (v HostinfoView) AppConnector() opt.Bool { return v.ж.AppConnector } + +// opaque hash of the most recent list of tailnet services, change in hash indicates config should be fetched via c2n +func (v HostinfoView) ServicesHash() string { return v.ж.ServicesHash } + +// the client’s selected exit node, empty when unselected. +func (v HostinfoView) ExitNodeID() StableNodeID { return v.ж.ExitNodeID } + +// Location represents geographical location data about a +// Tailscale host. Location is optional and only set if +// explicitly declared by a node. +func (v HostinfoView) Location() LocationView { return v.ж.Location.View() } + +// TPM device metadata, if available +func (v HostinfoView) TPM() views.ValuePointer[TPMInfo] { return views.ValuePointerOf(v.ж.TPM) } + +// StateEncrypted reports whether the node state is stored encrypted on +// disk. The actual mechanism is platform-specific: +// - Apple nodes use the Keychain +// - Linux and Windows nodes use the TPM +// - Android apps use EncryptedSharedPreferences func (v HostinfoView) StateEncrypted() opt.Bool { return v.ж.StateEncrypted } func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) } @@ -487,22 +737,74 @@ func (v *NetInfoView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// MappingVariesByDestIP says whether the host's NAT mappings +// vary based on the destination IP. func (v NetInfoView) MappingVariesByDestIP() opt.Bool { return v.ж.MappingVariesByDestIP } -func (v NetInfoView) HairPinning() opt.Bool { return v.ж.HairPinning } -func (v NetInfoView) WorkingIPv6() opt.Bool { return v.ж.WorkingIPv6 } -func (v NetInfoView) OSHasIPv6() opt.Bool { return v.ж.OSHasIPv6 } -func (v NetInfoView) WorkingUDP() opt.Bool { return v.ж.WorkingUDP } -func (v NetInfoView) WorkingICMPv4() opt.Bool { return v.ж.WorkingICMPv4 } -func (v NetInfoView) HavePortMap() bool { return v.ж.HavePortMap } -func (v NetInfoView) UPnP() opt.Bool { return v.ж.UPnP } -func (v NetInfoView) PMP() opt.Bool { return v.ж.PMP } -func (v NetInfoView) PCP() opt.Bool { return v.ж.PCP } -func (v NetInfoView) PreferredDERP() int { return v.ж.PreferredDERP } -func (v NetInfoView) LinkType() string { return v.ж.LinkType } +// HairPinning is their router does hairpinning. +// It reports true even if there's no NAT involved. +func (v NetInfoView) HairPinning() opt.Bool { return v.ж.HairPinning } + +// WorkingIPv6 is whether the host has IPv6 internet connectivity. +func (v NetInfoView) WorkingIPv6() opt.Bool { return v.ж.WorkingIPv6 } + +// OSHasIPv6 is whether the OS supports IPv6 at all, regardless of +// whether IPv6 internet connectivity is available. +func (v NetInfoView) OSHasIPv6() opt.Bool { return v.ж.OSHasIPv6 } + +// WorkingUDP is whether the host has UDP internet connectivity. +func (v NetInfoView) WorkingUDP() opt.Bool { return v.ж.WorkingUDP } + +// WorkingICMPv4 is whether ICMPv4 works. +// Empty means not checked. +func (v NetInfoView) WorkingICMPv4() opt.Bool { return v.ж.WorkingICMPv4 } + +// HavePortMap is whether we have an existing portmap open +// (UPnP, PMP, or PCP). +func (v NetInfoView) HavePortMap() bool { return v.ж.HavePortMap } + +// UPnP is whether UPnP appears present on the LAN. +// Empty means not checked. +func (v NetInfoView) UPnP() opt.Bool { return v.ж.UPnP } + +// PMP is whether NAT-PMP appears present on the LAN. +// Empty means not checked. +func (v NetInfoView) PMP() opt.Bool { return v.ж.PMP } + +// PCP is whether PCP appears present on the LAN. +// Empty means not checked. +func (v NetInfoView) PCP() opt.Bool { return v.ж.PCP } + +// PreferredDERP is this node's preferred (home) DERP region ID. +// This is where the node expects to be contacted to begin a +// peer-to-peer connection. The node might be be temporarily +// connected to multiple DERP servers (to speak to other nodes +// that are located elsewhere) but PreferredDERP is the region ID +// that the node subscribes to traffic at. +// Zero means disconnected or unknown. +func (v NetInfoView) PreferredDERP() int { return v.ж.PreferredDERP } + +// LinkType is the current link type, if known. +func (v NetInfoView) LinkType() string { return v.ж.LinkType } + +// DERPLatency is the fastest recent time to reach various +// DERP STUN servers, in seconds. The map key is the +// "regionID-v4" or "-v6"; it was previously the DERP server's +// STUN host:port. +// +// This should only be updated rarely, or when there's a +// material change, as any change here also gets uploaded to +// the control plane. func (v NetInfoView) DERPLatency() views.Map[string, float64] { return views.MapOf(v.ж.DERPLatency) } -func (v NetInfoView) FirewallMode() string { return v.ж.FirewallMode } -func (v NetInfoView) String() string { return v.ж.String() } + +// FirewallMode encodes both which firewall mode was selected and why. +// It is Linux-specific (at least as of 2023-08-19) and is meant to help +// debug iptables-vs-nftables issues. The string is of the form +// "{nft,ift}-REASON", like "nft-forced" or "ipt-default". Empty means +// either not Linux or a configuration in which the host firewall rules +// are not managed by tailscaled. +func (v NetInfoView) FirewallMode() string { return v.ж.FirewallMode } +func (v NetInfoView) String() string { return v.ж.String() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _NetInfoViewNeedsRegeneration = NetInfo(struct { @@ -589,10 +891,19 @@ func (v *LoginView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v LoginView) ID() LoginID { return v.ж.ID } -func (v LoginView) Provider() string { return v.ж.Provider } -func (v LoginView) LoginName() string { return v.ж.LoginName } -func (v LoginView) DisplayName() string { return v.ж.DisplayName } +// unused in the Tailscale client +func (v LoginView) ID() LoginID { return v.ж.ID } + +// "google", "github", "okta_foo", etc. +func (v LoginView) Provider() string { return v.ж.Provider } + +// an email address or "email-ish" string (like alice@github) +func (v LoginView) LoginName() string { return v.ж.LoginName } + +// from the IdP +func (v LoginView) DisplayName() string { return v.ж.DisplayName } + +// from the IdP func (v LoginView) ProfilePicURL() string { return v.ж.ProfilePicURL } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -672,26 +983,82 @@ func (v *DNSConfigView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// Resolvers are the DNS resolvers to use, in order of preference. func (v DNSConfigView) Resolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.Resolvers) } +// Routes maps DNS name suffixes to a set of DNS resolvers to +// use. It is used to implement "split DNS" and other advanced DNS +// routing overlays. +// +// Map keys are fully-qualified DNS name suffixes; they may +// optionally contain a trailing dot but no leading dot. +// +// If the value is an empty slice, that means the suffix should still +// be handled by Tailscale's built-in resolver (100.100.100.100), such +// as for the purpose of handling ExtraRecords. func (v DNSConfigView) Routes() views.MapFn[string, []*dnstype.Resolver, views.SliceView[*dnstype.Resolver, dnstype.ResolverView]] { return views.MapFnOf(v.ж.Routes, func(t []*dnstype.Resolver) views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](t) }) } + +// FallbackResolvers is like Resolvers, but is only used if a +// split DNS configuration is requested in a configuration that +// doesn't work yet without explicit default resolvers. +// https://github.com/tailscale/tailscale/issues/1743 func (v DNSConfigView) FallbackResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] { return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.FallbackResolvers) } -func (v DNSConfigView) Domains() views.Slice[string] { return views.SliceOf(v.ж.Domains) } -func (v DNSConfigView) Proxied() bool { return v.ж.Proxied } + +// Domains are the search domains to use. +// Search domains must be FQDNs, but *without* the trailing dot. +func (v DNSConfigView) Domains() views.Slice[string] { return views.SliceOf(v.ж.Domains) } + +// Proxied turns on automatic resolution of hostnames for devices +// in the network map, aka MagicDNS. +// Despite the (legacy) name, does not necessarily cause request +// proxying to be enabled. +func (v DNSConfigView) Proxied() bool { return v.ж.Proxied } + +// Nameservers are the IP addresses of the global nameservers to use. +// +// Deprecated: this is only set and used by MapRequest.Version >=9 and <14. Use Resolvers instead. func (v DNSConfigView) Nameservers() views.Slice[netip.Addr] { return views.SliceOf(v.ж.Nameservers) } -func (v DNSConfigView) CertDomains() views.Slice[string] { return views.SliceOf(v.ж.CertDomains) } + +// CertDomains are the set of DNS names for which the control +// plane server will assist with provisioning TLS +// certificates. See SetDNSRequest, which can be used to +// answer dns-01 ACME challenges for e.g. LetsEncrypt. +// These names are FQDNs without trailing periods, and without +// any "_acme-challenge." prefix. +func (v DNSConfigView) CertDomains() views.Slice[string] { return views.SliceOf(v.ж.CertDomains) } + +// ExtraRecords contains extra DNS records to add to the +// MagicDNS config. func (v DNSConfigView) ExtraRecords() views.Slice[DNSRecord] { return views.SliceOf(v.ж.ExtraRecords) } + +// ExitNodeFilteredSuffixes are the DNS suffixes that the +// node, when being an exit node DNS proxy, should not answer. +// +// The entries do not contain trailing periods and are always +// all lowercase. +// +// If an entry starts with a period, it's a suffix match (but +// suffix ".a.b" doesn't match "a.b"; a prefix is required). +// +// If an entry does not start with a period, it's an exact +// match. +// +// Matches are case insensitive. func (v DNSConfigView) ExitNodeFilteredSet() views.Slice[string] { return views.SliceOf(v.ж.ExitNodeFilteredSet) } + +// TempCorpIssue13969 is a temporary (2023-08-16) field for an internal hack day prototype. +// It contains a user inputed URL that should have a list of domains to be blocked. +// See https://github.com/tailscale/corp/issues/13969. func (v DNSConfigView) TempCorpIssue13969() string { return v.ж.TempCorpIssue13969 } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -775,14 +1142,26 @@ func (v *RegisterResponseView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v RegisterResponseView) User() User { return v.ж.User } -func (v RegisterResponseView) Login() Login { return v.ж.Login } -func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired } +func (v RegisterResponseView) User() User { return v.ж.User } +func (v RegisterResponseView) Login() Login { return v.ж.Login } + +// if true, the NodeKey needs to be replaced +func (v RegisterResponseView) NodeKeyExpired() bool { return v.ж.NodeKeyExpired } + +// TODO(crawshaw): move to using MachineStatus func (v RegisterResponseView) MachineAuthorized() bool { return v.ж.MachineAuthorized } -func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL } + +// if set, authorization pending +func (v RegisterResponseView) AuthURL() string { return v.ж.AuthURL } + +// If set, this is the current node-key signature that needs to be +// re-signed for the node's new node-key. func (v RegisterResponseView) NodeKeySignature() views.ByteSlice[tkatype.MarshaledSignature] { return views.ByteSliceOf(v.ж.NodeKeySignature) } + +// Error indicates that authorization failed. If this is non-empty, +// other status fields should be ignored. 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. @@ -863,6 +1242,7 @@ func (v *RegisterResponseAuthView) UnmarshalJSONFrom(dec *jsontext.Decoder) erro return nil } +// used by pre-1.66 Android only func (v RegisterResponseAuthView) Oauth2Token() views.ValuePointer[Oauth2Token] { return views.ValuePointerOf(v.ж.Oauth2Token) } @@ -943,29 +1323,69 @@ func (v *RegisterRequestView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// Version is the client's capabilities when using the Noise +// transport. +// +// When using the original nacl crypto_box transport, the +// value must be 1. func (v RegisterRequestView) Version() CapabilityVersion { return v.ж.Version } func (v RegisterRequestView) NodeKey() key.NodePublic { return v.ж.NodeKey } func (v RegisterRequestView) OldNodeKey() key.NodePublic { return v.ж.OldNodeKey } func (v RegisterRequestView) NLKey() key.NLPublic { return v.ж.NLKey } func (v RegisterRequestView) Auth() RegisterResponseAuthView { return v.ж.Auth.View() } -func (v RegisterRequestView) Expiry() time.Time { return v.ж.Expiry } -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 } + +// Expiry optionally specifies the requested key expiry. +// The server policy may override. +// As a special case, if Expiry is in the past and NodeKey is +// the node's current key, the key is expired. +func (v RegisterRequestView) Expiry() time.Time { return v.ж.Expiry } + +// response waits until AuthURL is visited +func (v RegisterRequestView) Followup() string { return v.ж.Followup } +func (v RegisterRequestView) Hostinfo() HostinfoView { return v.ж.Hostinfo.View() } + +// Ephemeral is whether the client is requesting that this +// node be considered ephemeral and be automatically deleted +// when it stops being active. +func (v RegisterRequestView) Ephemeral() bool { return v.ж.Ephemeral } + +// NodeKeySignature is the node's own node-key signature, re-signed +// for its new node key using its network-lock key. +// +// This field is set when the client retries registration after learning +// its NodeKeySignature (which is in need of rotation). func (v RegisterRequestView) NodeKeySignature() views.ByteSlice[tkatype.MarshaledSignature] { return views.ByteSliceOf(v.ж.NodeKeySignature) } + +// The following fields are not used for SignatureNone and are required for +// SignatureV1: func (v RegisterRequestView) SignatureType() SignatureType { return v.ж.SignatureType } + +// creation time of request to prevent replay func (v RegisterRequestView) Timestamp() views.ValuePointer[time.Time] { return views.ValuePointerOf(v.ж.Timestamp) } +// X.509 certificate for client device func (v RegisterRequestView) DeviceCert() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.DeviceCert) } + +// as described by SignatureType func (v RegisterRequestView) Signature() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Signature) } + +// Tailnet is an optional identifier specifying the name of the recommended or required +// network that the node should join. Its exact form should not be depended on; new +// forms are coming later. The identifier is generally a domain name (for an organization) +// or e-mail address (for a personal account on a shared e-mail provider). It is the same name +// used by the API, as described in /api.md#tailnet. +// If Tailnet begins with the prefix "required:" then the server should prevent logging in to a different +// network than the one specified. Otherwise, the server should recommend the specified network +// but still permit logging in to other networks. +// If empty, no recommendation is offered to the server and the login page should show all options. func (v RegisterRequestView) Tailnet() string { return v.ж.Tailnet } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -1055,6 +1475,19 @@ func (v *DERPHomeParamsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// RegionScore scales latencies of DERP regions by a given scaling +// factor when determining which region to use as the home +// ("preferred") DERP. Scores in the range (0, 1) will cause this +// region to be proportionally more preferred, and scores in the range +// (1, ∞) will penalize a region. +// +// If a region is not present in this map, it is treated as having a +// score of 1.0. +// +// Scores should not be 0 or negative; such scores will be ignored. +// +// A nil map means no change from the previous value (if any); an empty +// non-nil map can be sent to reset all scores back to 1.0. func (v DERPHomeParamsView) RegionScore() views.Map[int, float64] { return views.MapOf(v.ж.RegionScore) } @@ -1131,13 +1564,71 @@ func (v *DERPRegionView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v DERPRegionView) RegionID() int { return v.ж.RegionID } -func (v DERPRegionView) RegionCode() string { return v.ж.RegionCode } -func (v DERPRegionView) RegionName() string { return v.ж.RegionName } -func (v DERPRegionView) Latitude() float64 { return v.ж.Latitude } -func (v DERPRegionView) Longitude() float64 { return v.ж.Longitude } -func (v DERPRegionView) Avoid() bool { return v.ж.Avoid } +// RegionID is a unique integer for a geographic region. +// +// It corresponds to the legacy derpN.tailscale.com hostnames +// used by older clients. (Older clients will continue to resolve +// derpN.tailscale.com when contacting peers, rather than use +// the server-provided DERPMap) +// +// RegionIDs must be non-zero, positive, and guaranteed to fit +// in a JavaScript number. +// +// RegionIDs in range 900-999 are reserved for end users to run their +// own DERP nodes. +func (v DERPRegionView) RegionID() int { return v.ж.RegionID } + +// RegionCode is a short name for the region. It's usually a popular +// city or airport code in the region: "nyc", "sf", "sin", +// "fra", etc. +func (v DERPRegionView) RegionCode() string { return v.ж.RegionCode } + +// RegionName is a long English name for the region: "New York City", +// "San Francisco", "Singapore", "Frankfurt", etc. +func (v DERPRegionView) RegionName() string { return v.ж.RegionName } + +// Latitude, Longitude are optional geographical coordinates of the DERP region's city, in degrees. +func (v DERPRegionView) Latitude() float64 { return v.ж.Latitude } +func (v DERPRegionView) Longitude() float64 { return v.ж.Longitude } + +// Avoid is whether the client should avoid picking this as its home region. +// The region should only be used if a peer is there. Clients already using +// this region as their home should migrate away to a new region without +// Avoid set. +// +// Deprecated: because of bugs in past implementations combined with unclear +// docs that caused people to think the bugs were intentional, this field is +// deprecated. It was never supposed to cause STUN/DERP measurement probes, +// but due to bugs, it sometimes did. And then some parts of the code began +// to rely on that property. But then we were unable to use this field for +// its original purpose, nor its later imagined purpose, because various +// parts of the codebase thought it meant one thing and others thought it +// meant another. But it did something in the middle instead. So we're retiring +// it. Use NoMeasureNoHome instead. +func (v DERPRegionView) Avoid() bool { return v.ж.Avoid } + +// NoMeasureNoHome says that this regions should not be measured for its +// latency distance (STUN, HTTPS, etc) or availability (e.g. captive portal +// checks) and should never be selected as the node's home region. However, +// if a peer declares this region as its home, then this client is allowed +// to connect to it for the purpose of communicating with that peer. +// +// This is what the now deprecated Avoid bool was supposed to mean +// originally but had implementation bugs and documentation omissions. func (v DERPRegionView) NoMeasureNoHome() bool { return v.ж.NoMeasureNoHome } + +// Nodes are the DERP nodes running in this region, in +// priority order for the current client. Client TLS +// connections should ideally only go to the first entry +// (falling back to the second if necessary). STUN packets +// should go to the first 1 or 2. +// +// If nodes within a region route packets amongst themselves, +// but not to other regions. That said, each user/domain +// should get a the same preferred node order, so if all nodes +// for a user/network pick the first one (as they should, when +// things are healthy), the inter-cluster routing is minimal +// to zero. func (v DERPRegionView) Nodes() views.SliceView[*DERPNode, DERPNodeView] { return views.SliceOfViews[*DERPNode, DERPNodeView](v.ж.Nodes) } @@ -1221,13 +1712,26 @@ func (v *DERPMapView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// HomeParams, if non-nil, is a change in home parameters. +// +// The rest of the DEPRMap fields, if zero, means unchanged. func (v DERPMapView) HomeParams() DERPHomeParamsView { return v.ж.HomeParams.View() } +// Regions is the set of geographic regions running DERP node(s). +// +// It's keyed by the DERPRegion.RegionID. +// +// The numbers are not necessarily contiguous. func (v DERPMapView) Regions() views.MapFn[int, *DERPRegion, DERPRegionView] { return views.MapFnOf(v.ж.Regions, func(t *DERPRegion) DERPRegionView { return t.View() }) } + +// OmitDefaultRegions specifies to not use Tailscale's DERP servers, and only use those +// specified in this DERPMap. If there are none set outside of the defaults, this is a noop. +// +// This field is only meaningful if the Regions map is non-nil (indicating a change). func (v DERPMapView) OmitDefaultRegions() bool { return v.ж.OmitDefaultRegions } // A compilation failure here means this code must be regenerated, with the command at the top of this file. @@ -1304,18 +1808,74 @@ func (v *DERPNodeView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v DERPNodeView) Name() string { return v.ж.Name } -func (v DERPNodeView) RegionID() int { return v.ж.RegionID } -func (v DERPNodeView) HostName() string { return v.ж.HostName } -func (v DERPNodeView) CertName() string { return v.ж.CertName } -func (v DERPNodeView) IPv4() string { return v.ж.IPv4 } -func (v DERPNodeView) IPv6() string { return v.ж.IPv6 } -func (v DERPNodeView) STUNPort() int { return v.ж.STUNPort } -func (v DERPNodeView) STUNOnly() bool { return v.ж.STUNOnly } -func (v DERPNodeView) DERPPort() int { return v.ж.DERPPort } +// Name is a unique node name (across all regions). +// It is not a host name. +// It's typically of the form "1b", "2a", "3b", etc. (region +// ID + suffix within that region) +func (v DERPNodeView) Name() string { return v.ж.Name } + +// RegionID is the RegionID of the DERPRegion that this node +// is running in. +func (v DERPNodeView) RegionID() int { return v.ж.RegionID } + +// HostName is the DERP node's hostname. +// +// It is required but need not be unique; multiple nodes may +// have the same HostName but vary in configuration otherwise. +func (v DERPNodeView) HostName() string { return v.ж.HostName } + +// CertName optionally specifies the expected TLS cert common +// name. If empty, HostName is used. If CertName is non-empty, +// HostName is only used for the TCP dial (if IPv4/IPv6 are +// not present) + TLS ClientHello. +// +// As a special case, if CertName starts with "sha256-raw:", +// then the rest of the string is a hex-encoded SHA256 of the +// cert to expect. This is used for self-signed certs. +// In this case, the HostName field will typically be an IP +// address literal. +func (v DERPNodeView) CertName() string { return v.ж.CertName } + +// IPv4 optionally forces an IPv4 address to use, instead of using DNS. +// If empty, A record(s) from DNS lookups of HostName are used. +// If the string is not an IPv4 address, IPv4 is not used; the +// conventional string to disable IPv4 (and not use DNS) is +// "none". +func (v DERPNodeView) IPv4() string { return v.ж.IPv4 } + +// IPv6 optionally forces an IPv6 address to use, instead of using DNS. +// If empty, AAAA record(s) from DNS lookups of HostName are used. +// If the string is not an IPv6 address, IPv6 is not used; the +// conventional string to disable IPv6 (and not use DNS) is +// "none". +func (v DERPNodeView) IPv6() string { return v.ж.IPv6 } + +// Port optionally specifies a STUN port to use. +// Zero means 3478. +// To disable STUN on this node, use -1. +func (v DERPNodeView) STUNPort() int { return v.ж.STUNPort } + +// STUNOnly marks a node as only a STUN server and not a DERP +// server. +func (v DERPNodeView) STUNOnly() bool { return v.ж.STUNOnly } + +// DERPPort optionally provides an alternate TLS port number +// for the DERP HTTPS server. +// +// If zero, 443 is used. +func (v DERPNodeView) DERPPort() int { return v.ж.DERPPort } + +// InsecureForTests is used by unit tests to disable TLS verification. +// It should not be set by users. func (v DERPNodeView) InsecureForTests() bool { return v.ж.InsecureForTests } -func (v DERPNodeView) STUNTestIP() string { return v.ж.STUNTestIP } -func (v DERPNodeView) CanPort80() bool { return v.ж.CanPort80 } + +// STUNTestIP is used in tests to override the STUN server's IP. +// If empty, it's assumed to be the same as the DERP server. +func (v DERPNodeView) STUNTestIP() string { return v.ж.STUNTestIP } + +// CanPort80 specifies whether this DERP node is accessible over HTTP +// on port 80 specifically. This is used for captive portal checks. +func (v DERPNodeView) CanPort80() bool { return v.ж.CanPort80 } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _DERPNodeViewNeedsRegeneration = DERPNode(struct { @@ -1400,17 +1960,49 @@ func (v *SSHRuleView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// RuleExpires, if non-nil, is when this rule expires. +// +// For example, a (principal,sshuser) tuple might be granted +// prompt-free SSH access for N minutes, so this rule would be +// before a expiration-free rule for the same principal that +// required an auth prompt. This permits the control plane to +// be out of the path for already-authorized SSH pairs. +// +// Once a rule matches, the lifetime of any accepting connection +// is subject to the SSHAction.SessionExpires time, if any. func (v SSHRuleView) RuleExpires() views.ValuePointer[time.Time] { return views.ValuePointerOf(v.ж.RuleExpires) } +// Principals matches an incoming connection. If the connection +// matches anything in this list and also matches SSHUsers, +// then Action is applied. func (v SSHRuleView) Principals() views.SliceView[*SSHPrincipal, SSHPrincipalView] { return views.SliceOfViews[*SSHPrincipal, SSHPrincipalView](v.ж.Principals) } +// SSHUsers are the SSH users that this rule matches. It is a +// map from either ssh-user|"*" => local-user. The map must +// contain a key for either ssh-user or, as a fallback, "*" to +// match anything. If it does, the map entry's value is the +// actual user that's logged in. +// If the map value is the empty string (for either the +// requested SSH user or "*"), the rule doesn't match. +// If the map value is "=", it means the ssh-user should map +// directly to the local-user. +// It may be nil if the Action is reject. func (v SSHRuleView) SSHUsers() views.Map[string, string] { return views.MapOf(v.ж.SSHUsers) } -func (v SSHRuleView) Action() SSHActionView { return v.ж.Action.View() } -func (v SSHRuleView) AcceptEnv() views.Slice[string] { return views.SliceOf(v.ж.AcceptEnv) } + +// Action is the outcome to task. +// A nil or invalid action means to deny. +func (v SSHRuleView) Action() SSHActionView { return v.ж.Action.View() } + +// AcceptEnv is a slice of environment variable names that are allowlisted +// for the SSH rule in the policy file. +// +// AcceptEnv values may contain * and ? wildcard characters which match against +// an arbitrary number of characters or a single character respectively. +func (v SSHRuleView) AcceptEnv() views.Slice[string] { return views.SliceOf(v.ж.AcceptEnv) } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _SSHRuleViewNeedsRegeneration = SSHRule(struct { @@ -1488,15 +2080,61 @@ func (v *SSHActionView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v SSHActionView) Message() string { return v.ж.Message } -func (v SSHActionView) Reject() bool { return v.ж.Reject } -func (v SSHActionView) Accept() bool { return v.ж.Accept } -func (v SSHActionView) SessionDuration() time.Duration { return v.ж.SessionDuration } -func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding } -func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate } -func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding } -func (v SSHActionView) AllowRemotePortForwarding() bool { return v.ж.AllowRemotePortForwarding } +// Message, if non-empty, is shown to the user before the +// action occurs. +func (v SSHActionView) Message() string { return v.ж.Message } + +// Reject, if true, terminates the connection. This action +// has higher priority that Accept, if given. +// The reason this is exists is primarily so a response +// from HoldAndDelegate has a way to stop the poll. +func (v SSHActionView) Reject() bool { return v.ж.Reject } + +// Accept, if true, accepts the connection immediately +// without further prompts. +func (v SSHActionView) Accept() bool { return v.ж.Accept } + +// SessionDuration, if non-zero, is how long the session can stay open +// before being forcefully terminated. +func (v SSHActionView) SessionDuration() time.Duration { return v.ж.SessionDuration } + +// AllowAgentForwarding, if true, allows accepted connections to forward +// the ssh agent if requested. +func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding } + +// HoldAndDelegate, if non-empty, is a URL that serves an +// outcome verdict. The connection will be accepted and will +// block until the provided long-polling URL serves a new +// SSHAction JSON value. The URL must be fetched using the +// Noise transport (in package control/control{base,http}). +// If the long poll breaks before returning a complete HTTP +// response, it should be re-fetched as long as the SSH +// session is open. +// +// The following variables in the URL are expanded by tailscaled: +// +// - $SRC_NODE_IP (URL escaped) +// - $SRC_NODE_ID (Node.ID as int64 string) +// - $DST_NODE_IP (URL escaped) +// - $DST_NODE_ID (Node.ID as int64 string) +// - $SSH_USER (URL escaped, ssh user requested) +// - $LOCAL_USER (URL escaped, local user mapped) +func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate } + +// AllowLocalPortForwarding, if true, allows accepted connections +// to use local port forwarding if requested. +func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding } + +// AllowRemotePortForwarding, if true, allows accepted connections +// to use remote port forwarding if requested. +func (v SSHActionView) AllowRemotePortForwarding() bool { return v.ж.AllowRemotePortForwarding } + +// Recorders defines the destinations of the SSH session recorders. +// The recording will be uploaded to http://addr:port/record. func (v SSHActionView) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) } + +// OnRecorderFailure is the action to take if recording fails. +// If nil, the default action is to fail open. func (v SSHActionView) OnRecordingFailure() views.ValuePointer[SSHRecorderFailureAction] { return views.ValuePointerOf(v.ж.OnRecordingFailure) } @@ -1584,8 +2222,19 @@ func (v *SSHPrincipalView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { func (v SSHPrincipalView) Node() StableNodeID { return v.ж.Node } func (v SSHPrincipalView) NodeIP() string { return v.ж.NodeIP } -func (v SSHPrincipalView) UserLogin() string { return v.ж.UserLogin } -func (v SSHPrincipalView) Any() bool { return v.ж.Any } + +// email-ish: foo@example.com, bar@github +func (v SSHPrincipalView) UserLogin() string { return v.ж.UserLogin } + +// if true, match any connection +func (v SSHPrincipalView) Any() bool { return v.ж.Any } + +// UnusedPubKeys was public key support. It never became an official product +// feature and so as of 2024-12-12 is being removed. +// This stub exists to remind us not to re-use the JSON field name "pubKeys" +// in the future if we bring it back with different semantics. +// +// Deprecated: do not use. It does nothing. func (v SSHPrincipalView) UnusedPubKeys() views.Slice[string] { return views.SliceOf(v.ж.UnusedPubKeys) } @@ -1666,6 +2315,7 @@ func (v *ControlDialPlanView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// An empty list means the default: use DNS (unspecified which DNS). func (v ControlDialPlanView) Candidates() views.Slice[ControlIPCandidate] { return views.SliceOf(v.ж.Candidates) } @@ -1742,13 +2392,35 @@ func (v *LocationView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v LocationView) Country() string { return v.ж.Country } +// User friendly country name, with proper capitalization ("Canada") +func (v LocationView) Country() string { return v.ж.Country } + +// ISO 3166-1 alpha-2 in upper case ("CA") func (v LocationView) CountryCode() string { return v.ж.CountryCode } -func (v LocationView) City() string { return v.ж.City } -func (v LocationView) CityCode() string { return v.ж.CityCode } -func (v LocationView) Latitude() float64 { return v.ж.Latitude } -func (v LocationView) Longitude() float64 { return v.ж.Longitude } -func (v LocationView) Priority() int { return v.ж.Priority } + +// User friendly city name, with proper capitalization ("Squamish") +func (v LocationView) City() string { return v.ж.City } + +// CityCode is a short code representing the city in upper case. +// CityCode is used to disambiguate a city from another location +// with the same city name. It uniquely identifies a particular +// geographical location, within the tailnet. +// IATA, ICAO or ISO 3166-2 codes are recommended ("YSE") +func (v LocationView) CityCode() string { return v.ж.CityCode } + +// Latitude, Longitude are optional geographical coordinates of the node, in degrees. +// No particular accuracy level is promised; the coordinates may simply be the center of the city or country. +func (v LocationView) Latitude() float64 { return v.ж.Latitude } +func (v LocationView) Longitude() float64 { return v.ж.Longitude } + +// Priority determines the order of use of an exit node when a +// location based preference matches more than one exit node, +// the node with the highest priority wins. Nodes of equal +// probability may be selected arbitrarily. +// +// A value of 0 means the exit node does not have a priority +// preference. A negative int is not allowed. +func (v LocationView) Priority() int { return v.ж.Priority } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _LocationViewNeedsRegeneration = Location(struct { @@ -1828,8 +2500,12 @@ func (v *UserProfileView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v UserProfileView) ID() UserID { return v.ж.ID } -func (v UserProfileView) LoginName() string { return v.ж.LoginName } +func (v UserProfileView) ID() UserID { return v.ж.ID } + +// "alice@smith.com"; for display purposes only (provider is not listed) +func (v UserProfileView) LoginName() string { return v.ж.LoginName } + +// "Alice Smith" func (v UserProfileView) DisplayName() string { return v.ж.DisplayName } func (v UserProfileView) ProfilePicURL() string { return v.ж.ProfilePicURL } func (v UserProfileView) Equal(v2 UserProfileView) bool { return v.ж.Equal(v2.ж) } @@ -1909,9 +2585,18 @@ func (v *VIPServiceView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v VIPServiceView) Name() ServiceName { return v.ж.Name } +// Name is the name of the service. The Name uniquely identifies a service +// on a particular tailnet, and so also corresponds uniquely to the pair of +// IP addresses belonging to the VIP service. +func (v VIPServiceView) Name() ServiceName { return v.ж.Name } + +// Ports specify which ProtoPorts are made available by this node +// on the service's IPs. func (v VIPServiceView) Ports() views.Slice[ProtoPortRange] { return views.SliceOf(v.ж.Ports) } -func (v VIPServiceView) Active() bool { return v.ж.Active } + +// Active specifies whether new requests for the service should be +// sent to this node by control. +func (v VIPServiceView) Active() bool { return v.ж.Active } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _VIPServiceViewNeedsRegeneration = VIPService(struct { diff --git a/types/dnstype/dnstype_view.go b/types/dnstype/dnstype_view.go index 0704670a2..a983864d0 100644 --- a/types/dnstype/dnstype_view.go +++ b/types/dnstype/dnstype_view.go @@ -84,10 +84,35 @@ func (v *ResolverView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } +// Addr is the address of the DNS resolver, one of: +// - A plain IP address for a "classic" UDP+TCP DNS resolver. +// This is the common format as sent by the control plane. +// - An IP:port, for tests. +// - "https://resolver.com/path" for DNS over HTTPS; currently +// as of 2022-09-08 only used for certain well-known resolvers +// (see the publicdns package) for which the IP addresses to dial DoH are +// known ahead of time, so bootstrap DNS resolution is not required. +// - "http://node-address:port/path" for DNS over HTTP over WireGuard. This +// is implemented in the PeerAPI for exit nodes and app connectors. +// - [TODO] "tls://resolver.com" for DNS over TCP+TLS func (v ResolverView) Addr() string { return v.ж.Addr } + +// BootstrapResolution is an optional suggested resolution for the +// DoT/DoH resolver, if the resolver URL does not reference an IP +// address directly. +// BootstrapResolution may be empty, in which case clients should +// look up the DoT/DoH server using their local "classic" DNS +// resolver. +// +// As of 2022-09-08, BootstrapResolution is not yet used. func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] { return views.SliceOf(v.ж.BootstrapResolution) } + +// UseWithExitNode designates that this resolver should continue to be used when an +// exit node is in use. Normally, DNS resolution is delegated to the exit node but +// there are situations where it is preferable to still use a Split DNS server and/or +// global DNS server instead of the exit node. func (v ResolverView) UseWithExitNode() bool { return v.ж.UseWithExitNode } func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) } diff --git a/types/persist/persist_view.go b/types/persist/persist_view.go index 99a86a6a5..7d1507468 100644 --- a/types/persist/persist_view.go +++ b/types/persist/persist_view.go @@ -86,11 +86,18 @@ func (v *PersistView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey } +func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey } + +// needed to request key rotation func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey } func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile } func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey } func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID } + +// DisallowedTKAStateIDs stores the tka.State.StateID values which +// this node will not operate network lock on. This is used to +// prevent bootstrapping TKA onto a key authority which was forcibly +// disabled. func (v PersistView) DisallowedTKAStateIDs() views.Slice[string] { return views.SliceOf(v.ж.DisallowedTKAStateIDs) } diff --git a/types/prefs/prefs_example/prefs_example_view.go b/types/prefs/prefs_example/prefs_example_view.go index afc9f1781..6a1a36865 100644 --- a/types/prefs/prefs_example/prefs_example_view.go +++ b/types/prefs/prefs_example/prefs_example_view.go @@ -89,38 +89,68 @@ func (v *PrefsView) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return nil } -func (v PrefsView) ControlURL() prefs.Item[string] { return v.ж.ControlURL } -func (v PrefsView) RouteAll() prefs.Item[bool] { return v.ж.RouteAll } -func (v PrefsView) ExitNodeID() prefs.Item[tailcfg.StableNodeID] { return v.ж.ExitNodeID } -func (v PrefsView) ExitNodeIP() prefs.Item[netip.Addr] { return v.ж.ExitNodeIP } -func (v PrefsView) ExitNodePrior() tailcfg.StableNodeID { return v.ж.ExitNodePrior } -func (v PrefsView) ExitNodeAllowLANAccess() prefs.Item[bool] { return v.ж.ExitNodeAllowLANAccess } -func (v PrefsView) CorpDNS() prefs.Item[bool] { return v.ж.CorpDNS } -func (v PrefsView) RunSSH() prefs.Item[bool] { return v.ж.RunSSH } -func (v PrefsView) RunWebClient() prefs.Item[bool] { return v.ж.RunWebClient } -func (v PrefsView) WantRunning() prefs.Item[bool] { return v.ж.WantRunning } -func (v PrefsView) LoggedOut() prefs.Item[bool] { return v.ж.LoggedOut } -func (v PrefsView) ShieldsUp() prefs.Item[bool] { return v.ж.ShieldsUp } -func (v PrefsView) AdvertiseTags() prefs.ListView[string] { return v.ж.AdvertiseTags.View() } -func (v PrefsView) Hostname() prefs.Item[string] { return v.ж.Hostname } -func (v PrefsView) NotepadURLs() prefs.Item[bool] { return v.ж.NotepadURLs } -func (v PrefsView) ForceDaemon() prefs.Item[bool] { return v.ж.ForceDaemon } -func (v PrefsView) Egg() prefs.Item[bool] { return v.ж.Egg } +func (v PrefsView) ControlURL() prefs.Item[string] { return v.ж.ControlURL } +func (v PrefsView) RouteAll() prefs.Item[bool] { return v.ж.RouteAll } +func (v PrefsView) ExitNodeID() prefs.Item[tailcfg.StableNodeID] { return v.ж.ExitNodeID } +func (v PrefsView) ExitNodeIP() prefs.Item[netip.Addr] { return v.ж.ExitNodeIP } + +// ExitNodePrior is an internal state rather than a preference. +// It can be kept in the Prefs structure but should not be wrapped +// and is ignored by the [prefs] package. +func (v PrefsView) ExitNodePrior() tailcfg.StableNodeID { return v.ж.ExitNodePrior } +func (v PrefsView) ExitNodeAllowLANAccess() prefs.Item[bool] { return v.ж.ExitNodeAllowLANAccess } +func (v PrefsView) CorpDNS() prefs.Item[bool] { return v.ж.CorpDNS } +func (v PrefsView) RunSSH() prefs.Item[bool] { return v.ж.RunSSH } +func (v PrefsView) RunWebClient() prefs.Item[bool] { return v.ж.RunWebClient } +func (v PrefsView) WantRunning() prefs.Item[bool] { return v.ж.WantRunning } +func (v PrefsView) LoggedOut() prefs.Item[bool] { return v.ж.LoggedOut } +func (v PrefsView) ShieldsUp() prefs.Item[bool] { return v.ж.ShieldsUp } + +// AdvertiseTags is a preference whose value is a slice of strings. +// The value is atomic, and individual items in the slice should +// not be modified after the preference is set. +// Since the item type (string) is immutable, we can use [prefs.List]. +func (v PrefsView) AdvertiseTags() prefs.ListView[string] { return v.ж.AdvertiseTags.View() } +func (v PrefsView) Hostname() prefs.Item[string] { return v.ж.Hostname } +func (v PrefsView) NotepadURLs() prefs.Item[bool] { return v.ж.NotepadURLs } +func (v PrefsView) ForceDaemon() prefs.Item[bool] { return v.ж.ForceDaemon } +func (v PrefsView) Egg() prefs.Item[bool] { return v.ж.Egg } + +// AdvertiseRoutes is a preference whose value is a slice of netip.Prefix. +// The value is atomic, and individual items in the slice should +// not be modified after the preference is set. +// Since the item type (netip.Prefix) is immutable, we can use [prefs.List]. func (v PrefsView) AdvertiseRoutes() prefs.ListView[netip.Prefix] { return v.ж.AdvertiseRoutes.View() } func (v PrefsView) NoSNAT() prefs.Item[bool] { return v.ж.NoSNAT } func (v PrefsView) NoStatefulFiltering() prefs.Item[opt.Bool] { return v.ж.NoStatefulFiltering } func (v PrefsView) NetfilterMode() prefs.Item[preftype.NetfilterMode] { return v.ж.NetfilterMode } func (v PrefsView) OperatorUser() prefs.Item[string] { return v.ж.OperatorUser } func (v PrefsView) ProfileName() prefs.Item[string] { return v.ж.ProfileName } -func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate } -func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector } -func (v PrefsView) PostureChecking() prefs.Item[bool] { return v.ж.PostureChecking } -func (v PrefsView) NetfilterKind() prefs.Item[string] { return v.ж.NetfilterKind } + +// AutoUpdate contains auto-update preferences. +// Each preference in the group can be configured and managed individually. +func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate } + +// AppConnector contains app connector-related preferences. +// Each preference in the group can be configured and managed individually. +func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector } +func (v PrefsView) PostureChecking() prefs.Item[bool] { return v.ж.PostureChecking } +func (v PrefsView) NetfilterKind() prefs.Item[string] { return v.ж.NetfilterKind } + +// DriveShares is a preference whose value is a slice of *[drive.Share]. +// The value is atomic, and individual items in the slice should +// not be modified after the preference is set. +// Since the item type (*drive.Share) is mutable and implements [views.ViewCloner], +// we need to use [prefs.StructList] instead of [prefs.List]. func (v PrefsView) DriveShares() prefs.StructListView[*drive.Share, drive.ShareView] { return prefs.StructListViewOf(&v.ж.DriveShares) } func (v PrefsView) AllowSingleHosts() prefs.Item[marshalAsTrueInJSON] { return v.ж.AllowSingleHosts } -func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } + +// Persist is an internal state rather than a preference. +// It can be kept in the Prefs structure but should not be wrapped +// and is ignored by the [prefs] package. +func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() } // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _PrefsViewNeedsRegeneration = Prefs(struct { diff --git a/types/prefs/prefs_view_test.go b/types/prefs/prefs_view_test.go index 44c3beb87..8993cb535 100644 --- a/types/prefs/prefs_view_test.go +++ b/types/prefs/prefs_view_test.go @@ -95,6 +95,9 @@ func (v TestPrefsView) AddrItem() Item[netip.Addr] { return v.ж.A func (v TestPrefsView) StringStringMap() MapView[string, string] { return v.ж.StringStringMap.View() } func (v TestPrefsView) IntStringMap() MapView[int, string] { return v.ж.IntStringMap.View() } func (v TestPrefsView) AddrIntMap() MapView[netip.Addr, int] { return v.ж.AddrIntMap.View() } + +// Bundles are complex preferences that usually consist of +// multiple parameters that must be configured atomically. func (v TestPrefsView) Bundle1() ItemView[*TestBundle, TestBundleView] { return ItemViewOf(&v.ж.Bundle1) } @@ -116,6 +119,10 @@ func (v TestPrefsView) IntBundleMap() StructMapView[int, *TestBundle, TestBundle func (v TestPrefsView) AddrBundleMap() StructMapView[netip.Addr, *TestBundle, TestBundleView] { return StructMapViewOf(&v.ж.AddrBundleMap) } + +// Group is a nested struct that contains one or more preferences. +// Each preference in a group can be configured individually. +// Preference groups should be included directly rather than by pointers. func (v TestPrefsView) Group() TestPrefsGroup { return v.ж.Group } // A compilation failure here means this code must be regenerated, with the command at the top of this file.