mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +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.
|
// Linux netfilter.
|
||||||
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
|
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
|
||||||
|
|
||||||
|
// VIP services.
|
||||||
|
req("GET /vip-services"): handleC2NVIPServicesGet,
|
||||||
}
|
}
|
||||||
|
|
||||||
type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request)
|
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)
|
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) {
|
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||||
b.logf("c2n: GET /update received")
|
b.logf("c2n: GET /update received")
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@ -4888,6 +4889,14 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
|
|||||||
}
|
}
|
||||||
hi.SSH_HostKeys = sshHostKeys
|
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
|
// The Hostinfo.WantIngress field tells control whether this node wants to
|
||||||
// be wired up for ingress connections. If harmless if it's accidentally
|
// be wired up for ingress connections. If harmless if it's accidentally
|
||||||
// true; the actual policy is controlled in tailscaled by ServeConfig. But
|
// true; the actual policy is controlled in tailscaled by ServeConfig. But
|
||||||
@ -7485,3 +7494,42 @@ func maybeUsernameOf(actor ipnauth.Actor) string {
|
|||||||
}
|
}
|
||||||
return username
|
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/control/controlclient"
|
||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/drive/driveimpl"
|
"tailscale.com/drive/driveimpl"
|
||||||
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
@ -4464,3 +4465,90 @@ func TestConfigFileReload(t *testing.T) {
|
|||||||
t.Fatalf("got %q; want %q", hn, "bar")
|
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)
|
// - 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)
|
// - 106: 2024-09-03: fix panic regression from cryptokey routing change (65fe0ba7b5)
|
||||||
// - 107: 2024-10-30: add App Connector to conffile (PR #13942)
|
// - 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
|
type StableID string
|
||||||
|
|
||||||
@ -820,6 +821,7 @@ type Hostinfo struct {
|
|||||||
Userspace opt.Bool `json:",omitempty"` // if the client is running in userspace (netstack) mode
|
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
|
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
|
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
|
// Location represents geographical location data about a
|
||||||
// Tailscale host. Location is optional and only set if
|
// Tailscale host. Location is optional and only set if
|
||||||
@ -830,6 +832,26 @@ type Hostinfo struct {
|
|||||||
// require changes to Hostinfo.Equal.
|
// 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
|
// TailscaleSSHEnabled reports whether or not this node is acting as a
|
||||||
// Tailscale SSH server.
|
// Tailscale SSH server.
|
||||||
func (hi *Hostinfo) TailscaleSSHEnabled() bool {
|
func (hi *Hostinfo) TailscaleSSHEnabled() bool {
|
||||||
@ -1429,6 +1451,11 @@ type CapGrant struct {
|
|||||||
// user groups as Kubernetes user groups. This capability is read by
|
// user groups as Kubernetes user groups. This capability is read by
|
||||||
// peers that are Tailscale Kubernetes operator instances.
|
// peers that are Tailscale Kubernetes operator instances.
|
||||||
PeerCapabilityKubernetes PeerCapability = "tailscale.com/cap/kubernetes"
|
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
|
// 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
|
Userspace opt.Bool
|
||||||
UserspaceRouter opt.Bool
|
UserspaceRouter opt.Bool
|
||||||
AppConnector opt.Bool
|
AppConnector opt.Bool
|
||||||
|
ServicesHash string
|
||||||
Location *Location
|
Location *Location
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
|
@ -66,6 +66,7 @@ func TestHostinfoEqual(t *testing.T) {
|
|||||||
"Userspace",
|
"Userspace",
|
||||||
"UserspaceRouter",
|
"UserspaceRouter",
|
||||||
"AppConnector",
|
"AppConnector",
|
||||||
|
"ServicesHash",
|
||||||
"Location",
|
"Location",
|
||||||
}
|
}
|
||||||
if have := fieldsOf(reflect.TypeFor[Hostinfo]()); !reflect.DeepEqual(have, hiHandles) {
|
if have := fieldsOf(reflect.TypeFor[Hostinfo]()); !reflect.DeepEqual(have, hiHandles) {
|
||||||
@ -240,6 +241,16 @@ func TestHostinfoEqual(t *testing.T) {
|
|||||||
&Hostinfo{AppConnector: opt.Bool("false")},
|
&Hostinfo{AppConnector: opt.Bool("false")},
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
&Hostinfo{ServicesHash: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"},
|
||||||
|
&Hostinfo{ServicesHash: "73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049"},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&Hostinfo{ServicesHash: "084c799cd551dd1d8d5c5f9a5d593b2e931f5e36122ee5c793c1d08a19839cc0"},
|
||||||
|
&Hostinfo{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
got := tt.a.Equal(tt.b)
|
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) Userspace() opt.Bool { return v.ж.Userspace }
|
||||||
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
|
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
|
||||||
func (v HostinfoView) AppConnector() opt.Bool { return v.ж.AppConnector }
|
func (v HostinfoView) AppConnector() opt.Bool { return v.ж.AppConnector }
|
||||||
|
func (v HostinfoView) ServicesHash() string { return v.ж.ServicesHash }
|
||||||
func (v HostinfoView) Location() *Location {
|
func (v HostinfoView) Location() *Location {
|
||||||
if v.ж.Location == nil {
|
if v.ж.Location == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -365,6 +366,7 @@ func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
|||||||
Userspace opt.Bool
|
Userspace opt.Bool
|
||||||
UserspaceRouter opt.Bool
|
UserspaceRouter opt.Bool
|
||||||
AppConnector opt.Bool
|
AppConnector opt.Bool
|
||||||
|
ServicesHash string
|
||||||
Location *Location
|
Location *Location
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user