summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdriano Sela Aviles <adriano@tailscale.com>2026-04-21 13:13:05 -0700
committerAdriano Sela Aviles <adriano@tailscale.com>2026-04-22 15:58:49 -0700
commitce158b7bab421ab61f2730994618197cd7dd74d0 (patch)
tree3cee36673d6d2db46d83e389f303cd403592f1fc
parenta7d8aeb8aebc4bb01066eb6ffa69b9d8fe178b81 (diff)
downloadtailscale-adrianosela/corp-40648-extend-svcs-for-client-app-actions.tar.xz
tailscale-adrianosela/corp-40648-extend-svcs-for-client-app-actions.zip
tailcfg: extend services model for client application actionsadrianosela/corp-40648-extend-svcs-for-client-app-actions
Updates: tailscale/corp#40648 Signed-off-by: Adriano Sela Aviles <adriano@tailscale.com>
-rw-r--r--tailcfg/tailcfg.go52
-rw-r--r--tailcfg/tailcfg_test.go27
2 files changed, 79 insertions, 0 deletions
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index da218837a..2ad9c7139 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -3349,17 +3349,69 @@ const LBHeader = "Ts-Lb"
// this client is hosting can be ignored.
type ServiceIPMappings map[ServiceName][]netip.Addr
+// AppProto is an application-layer protocol identifier used in [Action].
+// It drives icon selection and client application matching in Tailscale clients.
+type AppProto string
+
+const (
+ AppProtoCockroachDB AppProto = "cockroachdb"
+ AppProtoHTTP AppProto = "http"
+ AppProtoKubernetes AppProto = "kubernetes"
+ AppProtoMongoDB AppProto = "mongodb"
+ AppProtoMySQL AppProto = "mysql"
+ AppProtoPostgreSQL AppProto = "postgresql"
+ AppProtoSSH AppProto = "ssh"
+ AppProtoTCP AppProto = "tcp"
+)
+
+// Valid reports whether a is a known application protocol.
+func (a AppProto) Valid() bool {
+ switch a {
+ case AppProtoCockroachDB, AppProtoHTTP, AppProtoKubernetes,
+ AppProtoMongoDB, AppProtoMySQL, AppProtoPostgreSQL,
+ AppProtoSSH, AppProtoTCP:
+ return true
+ }
+ return false
+}
+
+// Action describes an application-level action that a Tailscale client can
+// invoke for a [ServiceDetails].
+type Action struct {
+ // Label is the human-readable label shown in client menus.
+ Label string `json:",omitzero"`
+
+ // ApplicationProto is the application-layer protocol identifier.
+ // It drives icon selection and client application matching.
+ ApplicationProto AppProto
+
+ // Port is the target TCP port for this action. It must match one of
+ // the specific (non-range) TCP ports listed in the enclosing
+ // [ServiceDetails.Ports].
+ Port uint16
+}
+
// ServiceDetails describes a Service visible to this node.
// It is the value type stored under [NodeAttrPrefixServices]+serviceName keys in [NodeCapMap].
type ServiceDetails struct {
// Name is the name of the Service, of the form "svc:dns-label".
Name ServiceName
+ // DisplayName is an optional human-readable label for the service.
+ // If empty, Name is used as a fallback by clients.
+ DisplayName string `json:",omitzero"`
+
// Addrs are the IP addresses (IPv4 and IPv6) assigned to this Service.
Addrs []netip.Addr `json:",omitempty"`
// Ports are the protocol/port combinations the Service accepts.
Ports []ProtoPortRange `json:",omitempty"`
+
+ // Actions is an optional list of actions for this service. Each action
+ // maps an application protocol to a port; the port must be one of the
+ // entries in Ports. Not every port needs a corresponding action. When
+ // Actions is empty, clients may infer a default action from Ports.
+ Actions []Action `json:",omitzero"`
}
// ClientAuditAction represents an auditable action that a client can report to the
diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go
index 8dd9191b6..9cf125d8f 100644
--- a/tailcfg/tailcfg_test.go
+++ b/tailcfg/tailcfg_test.go
@@ -905,6 +905,33 @@ func TestCheckTag(t *testing.T) {
}
}
+func TestAppProtoValid(t *testing.T) {
+ tests := []struct {
+ proto AppProto
+ want bool
+ }{
+ {AppProtoCockroachDB, true},
+ {AppProtoHTTP, true},
+ {AppProtoKubernetes, true},
+ {AppProtoMongoDB, true},
+ {AppProtoMySQL, true},
+ {AppProtoPostgreSQL, true},
+ {AppProtoSSH, true},
+ {AppProtoTCP, true},
+ {"", false},
+ {"ftp", false},
+ {"https", false},
+ {"unknown", false},
+ }
+ for _, tt := range tests {
+ t.Run(string(tt.proto), func(t *testing.T) {
+ if got := tt.proto.Valid(); got != tt.want {
+ t.Errorf("AppProto(%q).Valid() = %v, want %v", tt.proto, got, tt.want)
+ }
+ })
+ }
+}
+
func TestDisplayMessageEqual(t *testing.T) {
type test struct {
name string