ipn/ipnlocal: connect serve config to c2n endpoint

This commit updates the VIPService c2n endpoint on client to response with actual VIPService configuration stored
in the serve config.

Fixes tailscale/corp#24510
Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
KevinLiang10 2025-01-06 11:27:11 -05:00
parent 60daa2adb8
commit 009da8a364
3 changed files with 154 additions and 34 deletions

View File

@ -11,6 +11,7 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -5017,13 +5018,7 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
} }
hi.SSH_HostKeys = sshHostKeys hi.SSH_HostKeys = sshHostKeys
services := vipServicesFromPrefs(prefs) hi.ServicesHash = b.vipServiceHashLocked(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
@ -7659,28 +7654,38 @@ func maybeUsernameOf(actor ipnauth.Actor) string {
func (b *LocalBackend) VIPServices() []*tailcfg.VIPService { func (b *LocalBackend) VIPServices() []*tailcfg.VIPService {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
return vipServicesFromPrefs(b.pm.CurrentPrefs()) return b.vipServicesFromPrefsLocked(b.pm.CurrentPrefs())
} }
func vipServicesFromPrefs(prefs ipn.PrefsView) []*tailcfg.VIPService { func (b *LocalBackend) vipServiceHashLocked(prefs ipn.PrefsView) string {
services := b.vipServicesFromPrefsLocked(prefs)
if len(services) == 0 {
return ""
}
buf, err := json.Marshal(services)
if err != nil {
b.logf("vipServiceHashLocked: %v", err)
return ""
}
hash := sha256.Sum256(buf)
return hex.EncodeToString(hash[:])
}
func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService {
// keyed by service name // keyed by service name
var services map[string]*tailcfg.VIPService var services map[string]*tailcfg.VIPService
if !b.serveConfig.Valid() {
// TODO(naman): this envknob will be replaced with service-specific port return nil
// 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 { for svc, config := range b.serveConfig.Services().All() {
mak.Set(&services, s, &tailcfg.VIPService{ mak.Set(&services, svc, &tailcfg.VIPService{
Name: s, Name: svc,
Ports: []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}}, Ports: config.ServicePortRange(),
}) })
} }
for _, s := range prefs.AdvertiseServices().AsSlice() { for _, s := range prefs.AdvertiseServices().All() {
if services == nil || services[s] == nil { if services == nil || services[s] == nil {
mak.Set(&services, s, &tailcfg.VIPService{ mak.Set(&services, s, &tailcfg.VIPService{
Name: s, Name: s,

View File

@ -30,7 +30,6 @@ import (
"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"
@ -4511,13 +4510,13 @@ func TestGetVIPServices(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
advertised []string advertised []string
mapped []string serveConfig *ipn.ServeConfig
want []*tailcfg.VIPService want []*tailcfg.VIPService
}{ }{
{ {
"advertised-only", "advertised-only",
[]string{"svc:abc", "svc:def"}, []string{"svc:abc", "svc:def"},
[]string{}, &ipn.ServeConfig{},
[]*tailcfg.VIPService{ []*tailcfg.VIPService{
{ {
Name: "svc:abc", Name: "svc:abc",
@ -4530,9 +4529,13 @@ func TestGetVIPServices(t *testing.T) {
}, },
}, },
{ {
"mapped-only", "served-only",
[]string{}, []string{},
[]string{"svc:abc"}, &ipn.ServeConfig{
Services: map[string]*ipn.ServiceConfig{
"svc:abc": {Tun: true},
},
},
[]*tailcfg.VIPService{ []*tailcfg.VIPService{
{ {
Name: "svc:abc", Name: "svc:abc",
@ -4541,9 +4544,13 @@ func TestGetVIPServices(t *testing.T) {
}, },
}, },
{ {
"mapped-and-advertised", "served-and-advertised",
[]string{"svc:abc"},
[]string{"svc:abc"}, []string{"svc:abc"},
&ipn.ServeConfig{
Services: map[string]*ipn.ServiceConfig{
"svc:abc": {Tun: true},
},
},
[]*tailcfg.VIPService{ []*tailcfg.VIPService{
{ {
Name: "svc:abc", Name: "svc:abc",
@ -4553,9 +4560,13 @@ func TestGetVIPServices(t *testing.T) {
}, },
}, },
{ {
"mapped-and-advertised-separately", "served-and-advertised-different-service",
[]string{"svc:def"}, []string{"svc:def"},
[]string{"svc:abc"}, &ipn.ServeConfig{
Services: map[string]*ipn.ServiceConfig{
"svc:abc": {Tun: true},
},
},
[]*tailcfg.VIPService{ []*tailcfg.VIPService{
{ {
Name: "svc:abc", Name: "svc:abc",
@ -4567,14 +4578,78 @@ func TestGetVIPServices(t *testing.T) {
}, },
}, },
}, },
{
"served-with-port-ranges-one-range-single",
[]string{},
&ipn.ServeConfig{
Services: map[string]*ipn.ServiceConfig{
"svc:abc": {TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTPS: true},
}},
},
},
[]*tailcfg.VIPService{
{
Name: "svc:abc",
Ports: []tailcfg.ProtoPortRange{{Proto: 6, Ports: tailcfg.PortRange{First: 80, Last: 80}}},
},
},
},
{
"served-with-port-ranges-one-range-multiple",
[]string{},
&ipn.ServeConfig{
Services: map[string]*ipn.ServiceConfig{
"svc:abc": {TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTPS: true},
81: {HTTPS: true},
82: {HTTPS: true},
}},
},
},
[]*tailcfg.VIPService{
{
Name: "svc:abc",
Ports: []tailcfg.ProtoPortRange{{Proto: 6, Ports: tailcfg.PortRange{First: 80, Last: 82}}},
},
},
},
{
"served-with-port-ranges-multiple-ranges",
[]string{},
&ipn.ServeConfig{
Services: map[string]*ipn.ServiceConfig{
"svc:abc": {TCP: map[uint16]*ipn.TCPPortHandler{
80: {HTTPS: true},
81: {HTTPS: true},
82: {HTTPS: true},
1212: {HTTPS: true},
1213: {HTTPS: true},
1214: {HTTPS: true},
}},
},
},
[]*tailcfg.VIPService{
{
Name: "svc:abc",
Ports: []tailcfg.ProtoPortRange{
{Proto: 6, Ports: tailcfg.PortRange{First: 80, Last: 82}},
{Proto: 6, Ports: tailcfg.PortRange{First: 1212, Last: 1214}},
},
},
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
envknob.Setenv("TS_DEBUG_ALLPORTS_SERVICES", strings.Join(tt.mapped, ",")) lb := newLocalBackendWithTestControl(t, false, func(tb testing.TB, opts controlclient.Options) controlclient.Client {
return newClient(tb, opts)
})
lb.serveConfig = tt.serveConfig.View()
prefs := &ipn.Prefs{ prefs := &ipn.Prefs{
AdvertiseServices: tt.advertised, AdvertiseServices: tt.advertised,
} }
got := vipServicesFromPrefs(prefs.View()) got := lb.vipServicesFromPrefsLocked(prefs.View())
slices.SortFunc(got, func(a, b *tailcfg.VIPService) int { slices.SortFunc(got, func(a, b *tailcfg.VIPService) int {
return strings.Compare(a.Name, b.Name) return strings.Compare(a.Name, b.Name)
}) })

View File

@ -16,7 +16,9 @@ import (
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/util/mak" "tailscale.com/util/mak"
"tailscale.com/util/set"
) )
// ServeConfigKey returns a StateKey that stores the // ServeConfigKey returns a StateKey that stores the
@ -655,3 +657,41 @@ func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool {
} }
return false return false
} }
// ServicePortRange returns the list of tailcfg.ProtoPortRange that represents
// the proto/ports pairs that are being served by the service.
//
// Right now Tun mode is the only thing supports UDP, otherwise serve only supports TCP.
func (v ServiceConfigView) ServicePortRange() []tailcfg.ProtoPortRange {
if v.Tun() {
// If the service is in Tun mode, means service accept TCP/UDP on all ports.
return []tailcfg.ProtoPortRange{{Ports: tailcfg.PortRangeAny}}
}
tcp := int(ipproto.TCP)
// Deduplicate the ports.
servePorts := make(set.Set[uint16])
for port := range v.TCP().All() {
if port > 0 {
servePorts.Add(uint16(port))
}
}
dedupedServePorts := servePorts.Slice()
slices.Sort(dedupedServePorts)
var ranges []tailcfg.ProtoPortRange
for _, p := range dedupedServePorts {
if n := len(ranges); n > 0 && p == ranges[n-1].Ports.Last+1 {
ranges[n-1].Ports.Last = p
continue
}
ranges = append(ranges, tailcfg.ProtoPortRange{
Proto: tcp,
Ports: tailcfg.PortRange{
First: p,
Last: p,
},
})
}
return ranges
}