summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNaman Sood <mail@nsood.in>2024-11-15 16:14:06 -0500
committerGitHub <noreply@github.com>2024-11-15 16:14:06 -0500
commitaefbed323f33e7e02ea87147e2264efcce39d3f6 (patch)
treecf77356ec08ce588a8ee46cd8effd2666eeb7515
parent1355f622beca0db5794201ab8802804ab1299e2f (diff)
downloadtailscale-aefbed323f33e7e02ea87147e2264efcce39d3f6.tar.xz
tailscale-aefbed323f33e7e02ea87147e2264efcce39d3f6.zip
ipn,tailcfg: add VIPService struct and c2n to fetch them from client (#14046)
* ipn,tailcfg: add VIPService struct and c2n to fetch them from client Updates tailscale/corp#22743, tailscale/corp#22955 Signed-off-by: Naman Sood <mail@nsood.in> * more review fixes Signed-off-by: Naman Sood <mail@nsood.in> * don't mention PeerCapabilityServicesDestination since it's currently unused Signed-off-by: Naman Sood <mail@nsood.in> --------- Signed-off-by: Naman Sood <mail@nsood.in>
-rw-r--r--ipn/ipnlocal/c2n.go9
-rw-r--r--ipn/ipnlocal/local.go48
-rw-r--r--ipn/ipnlocal/local_test.go88
-rw-r--r--tailcfg/tailcfg.go29
-rw-r--r--tailcfg/tailcfg_clone.go1
-rw-r--r--tailcfg/tailcfg_test.go11
-rw-r--r--tailcfg/tailcfg_view.go2
7 files changed, 187 insertions, 1 deletions
diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go
index 8380689d1..f3a4a3a3d 100644
--- a/ipn/ipnlocal/c2n.go
+++ b/ipn/ipnlocal/c2n.go
@@ -77,6 +77,9 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
// Linux netfilter.
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
+
+ // VIP services.
+ req("GET /vip-services"): handleC2NVIPServicesGet,
}
type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request)
@@ -269,6 +272,12 @@ func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.R
w.WriteHeader(http.StatusNoContent)
}
+func handleC2NVIPServicesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
+ b.logf("c2n: GET /vip-services received")
+
+ json.NewEncoder(w).Encode(b.VIPServices())
+}
+
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /update received")
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index 493762fcc..3c7296038 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -9,6 +9,7 @@ import (
"bytes"
"cmp"
"context"
+ "crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
@@ -4888,6 +4889,14 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
}
hi.SSH_HostKeys = sshHostKeys
+ services := vipServicesFromPrefs(prefs)
+ if len(services) > 0 {
+ buf, _ := json.Marshal(services)
+ hi.ServicesHash = fmt.Sprintf("%02x", sha256.Sum256(buf))
+ } else {
+ hi.ServicesHash = ""
+ }
+
// The Hostinfo.WantIngress field tells control whether this node wants to
// be wired up for ingress connections. If harmless if it's accidentally
// true; the actual policy is controlled in tailscaled by ServeConfig. But
@@ -7485,3 +7494,42 @@ func maybeUsernameOf(actor ipnauth.Actor) string {
}
return username
}
+
+// VIPServices returns the list of tailnet services that this node
+// is serving as a destination for.
+// The returned memory is owned by the caller.
+func (b *LocalBackend) VIPServices() []*tailcfg.VIPService {
+ b.mu.Lock()
+ defer b.mu.Unlock()
+ return vipServicesFromPrefs(b.pm.CurrentPrefs())
+}
+
+func vipServicesFromPrefs(prefs ipn.PrefsView) []*tailcfg.VIPService {
+ // keyed by service name
+ var services map[string]*tailcfg.VIPService
+
+ // TODO(naman): this envknob will be replaced with service-specific port
+ // information once we start storing that.
+ var allPortsServices []string
+ if env := envknob.String("TS_DEBUG_ALLPORTS_SERVICES"); env != "" {
+ allPortsServices = strings.Split(env, ",")
+ }
+
+ for _, s := range allPortsServices {
+ mak.Set(&services, s, &tailcfg.VIPService{
+ Name: s,
+ Ports: []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}},
+ })
+ }
+
+ for _, s := range prefs.AdvertiseServices().AsSlice() {
+ if services == nil || services[s] == nil {
+ mak.Set(&services, s, &tailcfg.VIPService{
+ Name: s,
+ })
+ }
+ services[s].Active = true
+ }
+
+ return slices.Collect(maps.Values(services))
+}
diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go
index 6dad2dba4..6d25a418f 100644
--- a/ipn/ipnlocal/local_test.go
+++ b/ipn/ipnlocal/local_test.go
@@ -30,6 +30,7 @@ import (
"tailscale.com/control/controlclient"
"tailscale.com/drive"
"tailscale.com/drive/driveimpl"
+ "tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
@@ -4464,3 +4465,90 @@ func TestConfigFileReload(t *testing.T) {
t.Fatalf("got %q; want %q", hn, "bar")
}
}
+
+func TestGetVIPServices(t *testing.T) {
+ tests := []struct {
+ name string
+ advertised []string
+ mapped []string
+ want []*tailcfg.VIPService
+ }{
+ {
+ "advertised-only",
+ []string{"svc:abc", "svc:def"},
+ []string{},
+ []*tailcfg.VIPService{
+ {
+ Name: "svc:abc",
+ Active: true,
+ },
+ {
+ Name: "svc:def",
+ Active: true,
+ },
+ },
+ },
+ {
+ "mapped-only",
+ []string{},
+ []string{"svc:abc"},
+ []*tailcfg.VIPService{
+ {
+ Name: "svc:abc",
+ Ports: []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}},
+ },
+ },
+ },
+ {
+ "mapped-and-advertised",
+ []string{"svc:abc"},
+ []string{"svc:abc"},
+ []*tailcfg.VIPService{
+ {
+ Name: "svc:abc",
+ Active: true,
+ Ports: []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}},
+ },
+ },
+ },
+ {
+ "mapped-and-advertised-separately",
+ []string{"svc:def"},
+ []string{"svc:abc"},
+ []*tailcfg.VIPService{
+ {
+ Name: "svc:abc",
+ Ports: []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}},
+ },
+ {
+ Name: "svc:def",
+ Active: true,
+ },
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ envknob.Setenv("TS_DEBUG_ALLPORTS_SERVICES", strings.Join(tt.mapped, ","))
+ prefs := &ipn.Prefs{
+ AdvertiseServices: tt.advertised,
+ }
+ got := vipServicesFromPrefs(prefs.View())
+ slices.SortFunc(got, func(a, b *tailcfg.VIPService) int {
+ return strings.Compare(a.Name, b.Name)
+ })
+ if !reflect.DeepEqual(tt.want, got) {
+ t.Logf("want:")
+ for _, s := range tt.want {
+ t.Logf("%+v", s)
+ }
+ t.Logf("got:")
+ for _, s := range got {
+ t.Logf("%+v", s)
+ }
+ t.Fail()
+ return
+ }
+ })
+ }
+}
diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go
index 9e39a4336..1b283a2fc 100644
--- a/tailcfg/tailcfg.go
+++ b/tailcfg/tailcfg.go
@@ -150,7 +150,8 @@ type CapabilityVersion int
// - 105: 2024-08-05: Fixed SSH behavior on systems that use busybox (issue #12849)
// - 106: 2024-09-03: fix panic regression from cryptokey routing change (65fe0ba7b5)
// - 107: 2024-10-30: add App Connector to conffile (PR #13942)
-const CurrentCapabilityVersion CapabilityVersion = 107
+// - 108: 2024-11-08: Client sends ServicesHash in Hostinfo, understands c2n GET /vip-services.
+const CurrentCapabilityVersion CapabilityVersion = 108
type StableID string
@@ -820,6 +821,7 @@ type Hostinfo struct {
Userspace opt.Bool `json:",omitempty"` // if the client is running in userspace (netstack) mode
UserspaceRouter opt.Bool `json:",omitempty"` // if the client's subnet router is running in userspace (netstack) mode
AppConnector opt.Bool `json:",omitempty"` // if the client is running the app-connector service
+ ServicesHash string `json:",omitempty"` // opaque hash of the most recent list of tailnet services, change in hash indicates config should be fetched via c2n
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
@@ -830,6 +832,26 @@ type Hostinfo struct {
// require changes to Hostinfo.Equal.
}
+// VIPService represents a service created on a tailnet from the
+// perspective of a node providing that service. These services
+// have an virtual IP (VIP) address pair distinct from the node's IPs.
+type VIPService struct {
+ // Name is the name of the service, of the form `svc:dns-label`.
+ // See CheckServiceName for a validation func.
+ // 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.
+ Name string
+
+ // Ports specify which ProtoPorts are made available by this node
+ // on the service's IPs.
+ Ports []ProtoPortRange
+
+ // Active specifies whether new requests for the service should be
+ // sent to this node by control.
+ Active bool
+}
+
// TailscaleSSHEnabled reports whether or not this node is acting as a
// Tailscale SSH server.
func (hi *Hostinfo) TailscaleSSHEnabled() bool {
@@ -1429,6 +1451,11 @@ const (
// user groups as Kubernetes user groups. This capability is read by
// peers that are Tailscale Kubernetes operator instances.
PeerCapabilityKubernetes PeerCapability = "tailscale.com/cap/kubernetes"
+
+ // PeerCapabilityServicesDestination grants a peer the ability to serve as
+ // a destination for a set of given VIP services, which is provided as the
+ // value of this key in NodeCapMap.
+ PeerCapabilityServicesDestination PeerCapability = "tailscale.com/cap/services-destination"
)
// NodeCapMap is a map of capabilities to their optional values. It is valid for
diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go
index 61564f3f8..f4f02c017 100644
--- a/tailcfg/tailcfg_clone.go
+++ b/tailcfg/tailcfg_clone.go
@@ -183,6 +183,7 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
Userspace opt.Bool
UserspaceRouter opt.Bool
AppConnector opt.Bool
+ ServicesHash string
Location *Location
}{})
diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go
index 0d0636677..9f8c418a1 100644
--- a/tailcfg/tailcfg_test.go
+++ b/tailcfg/tailcfg_test.go
@@ -66,6 +66,7 @@ func TestHostinfoEqual(t *testing.T) {
"Userspace",
"UserspaceRouter",
"AppConnector",
+ "ServicesHash",
"Location",
}
if have := fieldsOf(reflect.TypeFor[Hostinfo]()); !reflect.DeepEqual(have, hiHandles) {
@@ -240,6 +241,16 @@ func TestHostinfoEqual(t *testing.T) {
&Hostinfo{AppConnector: opt.Bool("false")},
false,
},
+ {
+ &Hostinfo{ServicesHash: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"},
+ &Hostinfo{ServicesHash: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"},
+ true,
+ },
+ {
+ &Hostinfo{ServicesHash: "084c799cd551dd1d8d5c5f9a5d593b2e931f5e36122ee5c793c1d08a19839cc0"},
+ &Hostinfo{},
+ false,
+ },
}
for i, tt := range tests {
got := tt.a.Equal(tt.b)
diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go
index a3e19b0dc..f275a6a9d 100644
--- a/tailcfg/tailcfg_view.go
+++ b/tailcfg/tailcfg_view.go
@@ -318,6 +318,7 @@ func (v HostinfoView) Cloud() string { return v.ж.Clou
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) Location() *Location {
if v.ж.Location == nil {
return nil
@@ -365,6 +366,7 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
Userspace opt.Bool
UserspaceRouter opt.Bool
AppConnector opt.Bool
+ ServicesHash string
Location *Location
}{})