mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 11:05:45 +00:00
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>
This commit is contained in:
parent
1355f622be
commit
aefbed323f
@ -77,6 +77,9 @@
|
||||
|
||||
// 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")
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
"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))
|
||||
}
|
||||
|
@ -30,6 +30,7 @@
|
||||
"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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +150,8 @@
|
||||
// - 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 @@ type CapGrant struct {
|
||||
// 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
|
||||
|
@ -183,6 +183,7 @@ func (src *Hostinfo) Clone() *Hostinfo {
|
||||
Userspace opt.Bool
|
||||
UserspaceRouter opt.Bool
|
||||
AppConnector opt.Bool
|
||||
ServicesHash string
|
||||
Location *Location
|
||||
}{})
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 @@ func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
||||
Userspace opt.Bool
|
||||
UserspaceRouter opt.Bool
|
||||
AppConnector opt.Bool
|
||||
ServicesHash string
|
||||
Location *Location
|
||||
}{})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user