mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
[vnet] leaf cluster support (#42600)
* [vnet] leaf cluster support * add comments on clusterConfig fields * add comment
This commit is contained in:
@@ -870,6 +870,11 @@ func (c *Client) VnetConfigServiceClient() vnet.VnetConfigServiceClient {
|
||||
return vnet.NewVnetConfigServiceClient(c.conn)
|
||||
}
|
||||
|
||||
// GetVnetConfig returns the singleton VnetConfig resource.
|
||||
func (c *Client) GetVnetConfig(ctx context.Context) (*vnet.VnetConfig, error) {
|
||||
return c.VnetConfigServiceClient().GetVnetConfig(ctx, &vnet.GetVnetConfigRequest{})
|
||||
}
|
||||
|
||||
// Ping gets basic info about the auth server.
|
||||
func (c *Client) Ping(ctx context.Context) (proto.PingResponse, error) {
|
||||
rsp, err := c.grpc.Ping(ctx, &proto.PingRequest{})
|
||||
|
||||
@@ -1430,6 +1430,7 @@ type ClientI interface {
|
||||
services.Integrations
|
||||
services.KubeWaitingContainer
|
||||
services.Notifications
|
||||
services.VnetConfigGetter
|
||||
types.Events
|
||||
|
||||
types.WebSessionsGetter
|
||||
|
||||
@@ -22,10 +22,15 @@ import (
|
||||
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
|
||||
)
|
||||
|
||||
// VnetConfigService is an interface for the VnetConfig service.
|
||||
type VnetConfigService interface {
|
||||
// VnetConfigGetter is an interface for getting the cluster singleton VnetConfig.
|
||||
type VnetConfigGetter interface {
|
||||
// GetVnetConfig returns the singleton VnetConfig resource.
|
||||
GetVnetConfig(context.Context) (*vnet.VnetConfig, error)
|
||||
}
|
||||
|
||||
// VnetConfigService is an interface for the VnetConfig service.
|
||||
type VnetConfigService interface {
|
||||
VnetConfigGetter
|
||||
|
||||
// CreateVnetConfig does basic validation and creates a VnetConfig resource.
|
||||
CreateVnetConfig(ctx context.Context, vnetConfig *vnet.VnetConfig) (*vnet.VnetConfig, error)
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
vnetproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
prehogv1alpha "github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha"
|
||||
apiteleterm "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
|
||||
@@ -360,16 +359,6 @@ func (p *appProvider) GetDialOptions(ctx context.Context, profileName string) (*
|
||||
return dialOpts, nil
|
||||
}
|
||||
|
||||
func (p *appProvider) GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnetproto.VnetConfig, error) {
|
||||
clusterClient, err := p.getCachedClient(ctx, profileName, leafClusterName)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
vnetConfigClient := clusterClient.AuthClient.VnetConfigServiceClient()
|
||||
vnetConfig, err := vnetConfigClient.GetVnetConfig(ctx, &vnetproto.GetVnetConfigRequest{})
|
||||
return vnetConfig, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// OnNewConnection submits a usage event once per appProvider lifetime.
|
||||
// That is, if a user makes multiple connections to a single app, OnNewConnection submits a single
|
||||
// event. This is to mimic how Connect submits events for its app gateways. This lets us compare
|
||||
|
||||
+90
-55
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
@@ -33,7 +34,6 @@ import (
|
||||
"github.com/gravitational/teleport"
|
||||
apiclient "github.com/gravitational/teleport/api/client"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
@@ -61,9 +61,6 @@ type AppProvider interface {
|
||||
// GetDialOptions returns ALPN dial options for the profile.
|
||||
GetDialOptions(ctx context.Context, profileName string) (*DialOptions, error)
|
||||
|
||||
// GetVnetConfig returns the cluster VnetConfig resource.
|
||||
GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error)
|
||||
|
||||
// OnNewConnection gets called whenever a new connection is about to be established through VNet.
|
||||
// By the time OnNewConnection, VNet has already verified that the user holds a valid cert for the
|
||||
// app.
|
||||
@@ -120,7 +117,7 @@ func NewTCPAppResolver(appProvider AppProvider, opts ...tcpAppResolverOption) (*
|
||||
opt(r)
|
||||
}
|
||||
r.clock = cmp.Or(r.clock, clockwork.NewRealClock())
|
||||
r.clusterConfigCache = newClusterConfigCache(appProvider.GetVnetConfig, r.clock)
|
||||
r.clusterConfigCache = newClusterConfigCache(appProvider.GetCachedClient, r.clock)
|
||||
r.customDNSZoneChecker = newCustomDNSZoneValidator(r.lookupTXT)
|
||||
return r, nil
|
||||
}
|
||||
@@ -155,63 +152,107 @@ func (r *TCPAppResolver) ResolveTCPHandler(ctx context.Context, fqdn string) (*T
|
||||
// This is a query for the proxy address, which we'll never want to handle.
|
||||
return nil, ErrNoTCPHandler
|
||||
}
|
||||
if match, err := r.fqdnMatchesProfile(ctx, profileName, fqdn); err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
} else if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
slog := r.slog.With("profile", profileName, "fqdn", fqdn)
|
||||
rootClient, err := r.appProvider.GetCachedClient(ctx, profileName, "")
|
||||
clusterClient, err := r.clusterClientForAppFQDN(ctx, profileName, fqdn)
|
||||
if err != nil {
|
||||
// The user might be logged out from this one cluster (and retryWithRelogin isn't working). Don't
|
||||
// return an error so that DNS resolution will be forwarded upstream instead of failing, to avoid
|
||||
// breaking e.g. web app access (we don't know if this is a web or TCP app yet because we can't
|
||||
// log in).
|
||||
if errors.Is(err, errNoMatch) {
|
||||
continue
|
||||
}
|
||||
// The user might be logged out from this one cluster (and retryWithRelogin isn't working). Log
|
||||
// the error but don't return it so that DNS resolution will be forwarded upstream instead of
|
||||
// failing, to avoid breaking e.g. web app access (we don't know if this is a web or TCP app yet
|
||||
// because we can't log in).
|
||||
slog.ErrorContext(ctx, "Failed to get teleport client.", "error", err)
|
||||
continue
|
||||
}
|
||||
return r.resolveTCPHandlerForCluster(ctx, slog, rootClient.CurrentCluster(), profileName, "", fqdn)
|
||||
|
||||
leafClusterName := ""
|
||||
if clusterClient.ClusterName() != profileName {
|
||||
leafClusterName = clusterClient.ClusterName()
|
||||
}
|
||||
|
||||
slog := r.slog.With("profile", profileName, "fqdn", fqdn, "leaf_cluster", leafClusterName)
|
||||
return r.resolveTCPHandlerForCluster(ctx, slog, clusterClient, profileName, leafClusterName, fqdn)
|
||||
}
|
||||
// fqdn did not match any profile, forward the request upstream.
|
||||
return nil, ErrNoTCPHandler
|
||||
}
|
||||
|
||||
func (r *TCPAppResolver) fqdnMatchesProfile(ctx context.Context, profileName, fqdn string) (bool, error) {
|
||||
if isSubdomain(fqdn, profileName) {
|
||||
// The queried app fqdn is a subdomain of the proxy address, this is a match.
|
||||
return true, nil
|
||||
}
|
||||
// Not a proxy address subdomain, must check custom DNS zones.
|
||||
var errNoMatch = errors.New("cluster does not match queried FQDN")
|
||||
|
||||
// TODO(nklaassen): support leaf clusters.
|
||||
vnetConfig, err := r.clusterConfigCache.getVnetConfig(ctx, profileName, "" /*leafClustername*/)
|
||||
if err != nil {
|
||||
// Good chance we're here because the user is not logged in to the profile.
|
||||
r.slog.ErrorContext(ctx, "Failed to get VnetConfig, not checking custom DNS zones.", "profile", profileName, "error", err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// TODO(nklaassen): support leaf clusters.
|
||||
func (r *TCPAppResolver) clusterClientForAppFQDN(ctx context.Context, profileName, fqdn string) (ClusterClient, error) {
|
||||
rootClient, err := r.appProvider.GetCachedClient(ctx, profileName, "")
|
||||
if err != nil {
|
||||
r.slog.ErrorContext(ctx, "Failed to get teleport client, not checking custom DNS zones.", "profile", profileName, "error", err)
|
||||
return false, nil
|
||||
r.slog.ErrorContext(ctx, "Failed to get root cluster client, apps in this cluster will not be resolved.", "profile", profileName, "error", err)
|
||||
return nil, errNoMatch
|
||||
}
|
||||
clusterName := rootClient.ClusterName()
|
||||
for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() {
|
||||
if !isSubdomain(fqdn, zone.GetSuffix()) {
|
||||
// The queried app fqdn is not a subdomain of this custom zone suffix, skip it.
|
||||
|
||||
if isSubdomain(fqdn, profileName) {
|
||||
// The queried app fqdn is direct subdomain of this cluster proxy address.
|
||||
return rootClient, nil
|
||||
}
|
||||
|
||||
leafClusters, err := getLeafClusters(ctx, rootClient)
|
||||
if err != nil {
|
||||
// Good chance we're here because the user is not logged in to the profile.
|
||||
r.slog.ErrorContext(ctx, "Failed to list leaf clusters, apps in this cluster will not be resolved.", "profile", profileName, "error", err)
|
||||
return nil, errNoMatch
|
||||
}
|
||||
|
||||
// Prefix with an empty string to represent the root cluster.
|
||||
allClusters := append([]string{""}, leafClusters...)
|
||||
for _, leafClusterName := range allClusters {
|
||||
clusterClient, err := r.appProvider.GetCachedClient(ctx, profileName, leafClusterName)
|
||||
if err != nil {
|
||||
r.slog.ErrorContext(ctx, "Failed to get cluster client, apps in this cluster will not be resolved.", "profile", profileName, "leaf_cluster", leafClusterName, "error", err)
|
||||
continue
|
||||
}
|
||||
// The queried app fqdn is a subdomain of this custom zone suffix. Check if the custom zone is valid.
|
||||
if err := r.customDNSZoneChecker.validate(ctx, clusterName, zone.GetSuffix()); err != nil {
|
||||
r.slog.ErrorContext(ctx, "Failed to validate custom DNS zone %q for cluster %q", "error", err)
|
||||
return false, trace.Wrap(err, "validating custom DNS zone")
|
||||
|
||||
clusterConfig, err := r.clusterConfigCache.getClusterConfig(ctx, clusterClient)
|
||||
if err != nil {
|
||||
r.slog.ErrorContext(ctx, "Failed to get VnetConfig, apps in the cluster will not be resolved.", "profile", profileName, "leaf_cluster", leafClusterName, "error", err)
|
||||
continue
|
||||
}
|
||||
for _, zone := range clusterConfig.dnsZones {
|
||||
if !isSubdomain(fqdn, zone) {
|
||||
// The queried app fqdn is not a subdomain of this zone, skip it.
|
||||
continue
|
||||
}
|
||||
|
||||
// Found a matching cluster.
|
||||
|
||||
if zone == clusterConfig.proxyPublicAddr {
|
||||
// We don't need to validate a custom DNS zone if this is the proxy public address, this is a
|
||||
// normal app public_addr.
|
||||
return clusterClient, nil
|
||||
}
|
||||
// The queried app fqdn is a subdomain of this custom zone. Check if the zone is valid.
|
||||
if err := r.customDNSZoneChecker.validate(ctx, clusterConfig.clusterName, zone); err != nil {
|
||||
// Return an error here since the FQDN does match this custom zone, but the zone failed to
|
||||
// validate.
|
||||
return nil, trace.Wrap(err, "validating custom DNS zone %q matching queried FQDN %q", zone, fqdn)
|
||||
}
|
||||
return clusterClient, nil
|
||||
}
|
||||
}
|
||||
return nil, errNoMatch
|
||||
}
|
||||
|
||||
func getLeafClusters(ctx context.Context, rootClient ClusterClient) ([]string, error) {
|
||||
var leafClusters []string
|
||||
nextPage := ""
|
||||
for {
|
||||
remoteClusters, nextPage, err := rootClient.CurrentCluster().ListRemoteClusters(ctx, 0, nextPage)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
for _, rc := range remoteClusters {
|
||||
leafClusters = append(leafClusters, rc.GetName())
|
||||
}
|
||||
if nextPage == "" {
|
||||
return leafClusters, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// resolveTCPHandlerForCluster takes a cluster client and resolves [fqdn] to a [TCPHandlerSpec] if a matching
|
||||
@@ -221,13 +262,13 @@ func (r *TCPAppResolver) fqdnMatchesProfile(ctx context.Context, profileName, fq
|
||||
func (r *TCPAppResolver) resolveTCPHandlerForCluster(
|
||||
ctx context.Context,
|
||||
slog *slog.Logger,
|
||||
clt apiclient.GetResourcesClient,
|
||||
clusterClient ClusterClient,
|
||||
profileName, leafClusterName, fqdn string,
|
||||
) (*TCPHandlerSpec, error) {
|
||||
// An app public_addr could technically be full-qualified or not, match either way.
|
||||
expr := fmt.Sprintf(`(resource.spec.public_addr == "%s" || resource.spec.public_addr == "%s") && hasPrefix(resource.spec.uri, "tcp://")`,
|
||||
strings.TrimSuffix(fqdn, "."), fqdn)
|
||||
resp, err := apiclient.GetResourcePage[types.AppServer](ctx, clt, &proto.ListResourcesRequest{
|
||||
resp, err := apiclient.GetResourcePage[types.AppServer](ctx, clusterClient.CurrentCluster(), &proto.ListResourcesRequest{
|
||||
ResourceType: types.KindAppServer,
|
||||
PredicateExpression: expr,
|
||||
Limit: 1,
|
||||
@@ -248,19 +289,13 @@ func (r *TCPAppResolver) resolveTCPHandlerForCluster(
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
var cidrRange string
|
||||
vnetConfig, err := r.clusterConfigCache.getVnetConfig(ctx, profileName, leafClusterName)
|
||||
switch {
|
||||
case err == nil:
|
||||
cidrRange = cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange)
|
||||
case trace.IsNotFound(err) || trace.IsNotImplemented(err):
|
||||
cidrRange = defaultIPv4CIDRRange
|
||||
default:
|
||||
clusterConfig, err := r.clusterConfigCache.getClusterConfig(ctx, clusterClient)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &TCPHandlerSpec{
|
||||
IPv4CIDRRange: cidrRange,
|
||||
IPv4CIDRRange: clusterConfig.ipv4CIDRRange,
|
||||
TCPHandler: appHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -17,25 +17,38 @@
|
||||
package vnet
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
|
||||
)
|
||||
|
||||
type getClusterConfigFunc = func(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error)
|
||||
type getClusterClientFunc = func(ctx context.Context, profileName, leafClusterName string) (ClusterClient, error)
|
||||
|
||||
type cacheEntry struct {
|
||||
vnetConfig *vnet.VnetConfig
|
||||
expires time.Time
|
||||
type clusterConfig struct {
|
||||
// clusterName is the name of the cluster as reported by Ping.
|
||||
clusterName string
|
||||
// proxyPublicAddr is the public address of the proxy as reported by Ping, with any ports removed, this is
|
||||
// just the hostname. This is often but not always identical to the clusterName. For root clusters this
|
||||
// will be the same as the profile name (the profile is named after the proxy public addr).
|
||||
proxyPublicAddr string
|
||||
// dnsZones is the list of DNS zones that are valid for this cluster, this includes proxyPublicAddr *and*
|
||||
// any configured custom DNS zones for the cluster.
|
||||
dnsZones []string
|
||||
// ipv4CIDRRange is the CIDR range that IPv4 addresses should be assigned from for apps in this cluster.
|
||||
ipv4CIDRRange string
|
||||
// expires is the time at which this information should be considered stale and refetched. Stale data may
|
||||
// be used if a subsequent fetch fails.
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
func (e *cacheEntry) stale(clock clockwork.Clock) bool {
|
||||
func (e *clusterConfig) stale(clock clockwork.Clock) bool {
|
||||
return clock.Now().After(e.expires)
|
||||
}
|
||||
|
||||
@@ -48,66 +61,90 @@ func (e *cacheEntry) stale(clock clockwork.Clock) bool {
|
||||
type clusterConfigCache struct {
|
||||
flightGroup singleflight.Group
|
||||
clock clockwork.Clock
|
||||
get getClusterConfigFunc
|
||||
cache map[string]cacheEntry
|
||||
getClient getClusterClientFunc
|
||||
cache map[string]*clusterConfig
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newClusterConfigCache(get getClusterConfigFunc, clock clockwork.Clock) *clusterConfigCache {
|
||||
func newClusterConfigCache(getClient getClusterClientFunc, clock clockwork.Clock) *clusterConfigCache {
|
||||
return &clusterConfigCache{
|
||||
clock: clock,
|
||||
get: get,
|
||||
cache: make(map[string]cacheEntry),
|
||||
clock: clock,
|
||||
getClient: getClient,
|
||||
cache: make(map[string]*clusterConfig),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterConfigCache) getVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) {
|
||||
k := clusterCacheKey(profileName, leafClusterName)
|
||||
func (c *clusterConfigCache) getClusterConfig(ctx context.Context, clusterClient ClusterClient) (*clusterConfig, error) {
|
||||
k := clusterClient.ClusterName()
|
||||
|
||||
// Use a singleflight.Group to avoid concurrent requests for the same cluster VnetConfig.
|
||||
vnetConfig, err, _ := c.flightGroup.Do(k, func() (any, error) {
|
||||
result, err, _ := c.flightGroup.Do(k, func() (any, error) {
|
||||
// Check the cache inside flightGroup.Do to avoid the chance of immediate repeat calls to the cluster.
|
||||
c.mu.RLock()
|
||||
existingCacheEntry, existingCacheEntryFound := c.cache[k]
|
||||
c.mu.RUnlock()
|
||||
if existingCacheEntryFound && !existingCacheEntry.stale(c.clock) {
|
||||
return existingCacheEntry.vnetConfig, nil
|
||||
return existingCacheEntry, nil
|
||||
}
|
||||
|
||||
vnetConfig, err := c.get(ctx, profileName, leafClusterName)
|
||||
if trace.IsNotFound(err) || trace.IsNotImplemented(err) {
|
||||
// Default to the empty config on NotFound or NotImplemented.
|
||||
vnetConfig = &vnet.VnetConfig{}
|
||||
err = nil
|
||||
}
|
||||
clusterConfig, err := c.getClusterConfigUncached(ctx, clusterClient)
|
||||
if err != nil {
|
||||
// It's better to return a stale cached VnetConfig than an error. The profile probably expired and
|
||||
// we want to keep functioning until a relogin. We don't expect the VnetConfig to change very
|
||||
// often.
|
||||
if existingCacheEntryFound {
|
||||
return existingCacheEntry.vnetConfig, nil
|
||||
return existingCacheEntry, nil
|
||||
}
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.cache[k] = cacheEntry{
|
||||
vnetConfig: vnetConfig,
|
||||
expires: c.clock.Now().Add(5 * time.Minute),
|
||||
}
|
||||
c.cache[k] = clusterConfig
|
||||
c.mu.Unlock()
|
||||
|
||||
return vnetConfig, nil
|
||||
return clusterConfig, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return vnetConfig.(*vnet.VnetConfig), nil
|
||||
return result.(*clusterConfig), nil
|
||||
}
|
||||
|
||||
func clusterCacheKey(profileName, leafClusterName string) string {
|
||||
if leafClusterName != "" {
|
||||
return profileName + "/" + leafClusterName
|
||||
func (c *clusterConfigCache) getClusterConfigUncached(ctx context.Context, clusterClient ClusterClient) (*clusterConfig, error) {
|
||||
pingResp, err := clusterClient.CurrentCluster().Ping(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return profileName
|
||||
|
||||
clusterName := pingResp.ClusterName
|
||||
proxyPublicAddr := pingResp.ProxyPublicAddr
|
||||
if strings.Contains(proxyPublicAddr, ":") {
|
||||
proxyPublicAddr, _, err = net.SplitHostPort(pingResp.ProxyPublicAddr)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "parsing proxy public addr")
|
||||
}
|
||||
}
|
||||
|
||||
dnsZones := []string{proxyPublicAddr}
|
||||
ipv4CIDRRange := defaultIPv4CIDRRange
|
||||
|
||||
vnetConfig, err := clusterClient.CurrentCluster().GetVnetConfig(ctx)
|
||||
if trace.IsNotFound(err) || trace.IsNotImplemented(err) {
|
||||
// Use the defaults set above, nothing to do here.
|
||||
} else if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
} else {
|
||||
for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() {
|
||||
dnsZones = append(dnsZones, zone.GetSuffix())
|
||||
}
|
||||
ipv4CIDRRange = cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange)
|
||||
}
|
||||
|
||||
return &clusterConfig{
|
||||
clusterName: clusterName,
|
||||
proxyPublicAddr: proxyPublicAddr,
|
||||
dnsZones: dnsZones,
|
||||
ipv4CIDRRange: ipv4CIDRRange,
|
||||
expires: c.clock.Now().Add(5 * time.Minute),
|
||||
}, nil
|
||||
}
|
||||
|
||||
+41
-31
@@ -17,17 +17,14 @@
|
||||
package vnet
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
|
||||
"github.com/gravitational/teleport/api/profile"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils"
|
||||
@@ -71,7 +68,7 @@ func newOSConfigurator(tunName, ipv6Prefix, dnsAddr string) (*osConfigurator, er
|
||||
homePath: homePath,
|
||||
clientStore: client.NewFSClientStore(homePath),
|
||||
}
|
||||
configurator.clusterConfigCache = newClusterConfigCache(configurator.getVnetConfig, clockwork.NewRealClock())
|
||||
configurator.clusterConfigCache = newClusterConfigCache(configurator.getClusterClient, clockwork.NewRealClock())
|
||||
|
||||
return configurator, nil
|
||||
}
|
||||
@@ -85,8 +82,14 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error {
|
||||
return trace.Wrap(err, "listing user profiles")
|
||||
}
|
||||
for _, profileName := range profileNames {
|
||||
// TODO(nklaassen): support leaf clusters
|
||||
vnetConfig, err := c.clusterConfigCache.getVnetConfig(ctx, profileName, "" /*leafClusterName*/)
|
||||
rootClient, err := c.getClusterClient(ctx, profileName, "" /*leafClusterName*/)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx,
|
||||
"Failed to create root cluster client, profile may be expired, not configuring VNet for this cluster",
|
||||
"profile", profileName, "error", err)
|
||||
continue
|
||||
}
|
||||
clusterConfig, err := c.clusterConfigCache.getClusterConfig(ctx, rootClient)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx,
|
||||
"Failed to load VNet configuration, profile may be expired, not configuring VNet for this cluster",
|
||||
@@ -94,18 +97,36 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// profileName is the web proxy address, add the default DNS zone for it.
|
||||
// TODO(nklaassen): add the custom DNS zones as well, after the rest of VNet supports it.
|
||||
dnsZones = append(dnsZones, profileName)
|
||||
for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() {
|
||||
suffix := zone.GetSuffix()
|
||||
// Trim any leading or trailing "." to match expected format.
|
||||
zone := strings.TrimPrefix(strings.TrimSuffix(suffix, "."), ".")
|
||||
dnsZones = append(dnsZones, zone)
|
||||
}
|
||||
dnsZones = append(dnsZones, clusterConfig.dnsZones...)
|
||||
cidrRanges = append(cidrRanges, clusterConfig.ipv4CIDRRange)
|
||||
|
||||
cidrRange := cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange)
|
||||
cidrRanges = append(cidrRanges, cidrRange)
|
||||
leafClusters, err := getLeafClusters(ctx, rootClient)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx,
|
||||
"Failed to list leaf clusters, profile may be expired, not configuring VNet for this cluster",
|
||||
"profile", profileName, "error", err)
|
||||
continue
|
||||
}
|
||||
for _, leafClusterName := range leafClusters {
|
||||
clusterClient, err := c.getClusterClient(ctx, profileName, leafClusterName)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx,
|
||||
"Failed to create leaf cluster client, not configuring VNet for this cluster",
|
||||
"profile", profileName, "leaf_cluster", leafClusterName, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
clusterConfig, err := c.clusterConfigCache.getClusterConfig(ctx, clusterClient)
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx,
|
||||
"Failed to load VNet configuration, not configuring VNet for this cluster",
|
||||
"profile", profileName, "leaf_cluster", leafClusterName, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
dnsZones = append(dnsZones, clusterConfig.dnsZones...)
|
||||
cidrRanges = append(cidrRanges, clusterConfig.ipv4CIDRRange)
|
||||
}
|
||||
}
|
||||
|
||||
dnsZones = utils.Deduplicate(dnsZones)
|
||||
@@ -153,17 +174,7 @@ func (c *osConfigurator) setTunIPv4FromCIDR(cidrRange string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *osConfigurator) getVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) {
|
||||
clt, err := c.vnetConfigClient(ctx, profileName, leafClusterName)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "getting vnet client for profile %s %s", profileName, leafClusterName)
|
||||
}
|
||||
|
||||
vnetConfig, err := clt.GetVnetConfig(ctx, &vnet.GetVnetConfigRequest{})
|
||||
return vnetConfig, trace.Wrap(err)
|
||||
}
|
||||
|
||||
func (c *osConfigurator) vnetConfigClient(ctx context.Context, profileName, leafClusterName string) (vnet.VnetConfigServiceClient, error) {
|
||||
func (c *osConfigurator) getClusterClient(ctx context.Context, profileName, leafClusterName string) (ClusterClient, error) {
|
||||
// This runs in the root process, so obviously we don't have access to the client cache in the user
|
||||
// process. This loads cluster profiles and credentials from TELEPORT_HOME.
|
||||
clientConfig := &client.Config{
|
||||
@@ -180,11 +191,10 @@ func (c *osConfigurator) vnetConfigClient(ctx context.Context, profileName, leaf
|
||||
return nil, trace.Wrap(err, "creating new teleport client")
|
||||
}
|
||||
|
||||
clusterClt, err := tc.ConnectToCluster(ctx)
|
||||
clt, err := tc.ConnectToCluster(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "connecting to cluster")
|
||||
}
|
||||
|
||||
vnetConfigClt := clusterClt.CurrentCluster().VnetConfigServiceClient()
|
||||
return vnetConfigClt, nil
|
||||
return clt, nil
|
||||
}
|
||||
|
||||
+59
-14
@@ -291,9 +291,9 @@ func (p *echoAppProvider) GetCachedClient(ctx context.Context, profileName, leaf
|
||||
}
|
||||
if leafClusterName == "" {
|
||||
return &fakeClusterClient{
|
||||
clusterName: profileName,
|
||||
authClient: &fakeAuthClient{
|
||||
apps: rootCluster.apps,
|
||||
clusterSpec: rootCluster,
|
||||
clusterName: profileName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -302,9 +302,9 @@ func (p *echoAppProvider) GetCachedClient(ctx context.Context, profileName, leaf
|
||||
return nil, trace.NotFound("no cluster for %s.%s", profileName, leafClusterName)
|
||||
}
|
||||
return &fakeClusterClient{
|
||||
clusterName: leafClusterName,
|
||||
authClient: &fakeAuthClient{
|
||||
apps: leafCluster.apps,
|
||||
clusterSpec: leafCluster,
|
||||
clusterName: leafClusterName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -374,8 +374,7 @@ func (p *echoAppProvider) OnNewConnection(ctx context.Context, profileName, leaf
|
||||
}
|
||||
|
||||
type fakeClusterClient struct {
|
||||
authClient *fakeAuthClient
|
||||
clusterName string
|
||||
authClient *fakeAuthClient
|
||||
}
|
||||
|
||||
func (c *fakeClusterClient) CurrentCluster() authclient.ClientI {
|
||||
@@ -383,19 +382,20 @@ func (c *fakeClusterClient) CurrentCluster() authclient.ClientI {
|
||||
}
|
||||
|
||||
func (c *fakeClusterClient) ClusterName() string {
|
||||
return c.clusterName
|
||||
return c.authClient.clusterName
|
||||
}
|
||||
|
||||
// fakeAuthClient is a fake auth client that answers GetResources requests with a static list of apps and
|
||||
// basic/faked predicate filtering.
|
||||
type fakeAuthClient struct {
|
||||
authclient.ClientI
|
||||
apps []string
|
||||
clusterSpec testClusterSpec
|
||||
clusterName string
|
||||
}
|
||||
|
||||
func (c *fakeAuthClient) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) {
|
||||
resp := &proto.ListResourcesResponse{}
|
||||
for _, app := range c.apps {
|
||||
for _, app := range c.clusterSpec.apps {
|
||||
// Poor-man's predicate expression filter.
|
||||
if !strings.Contains(req.PredicateExpression, app) {
|
||||
continue
|
||||
@@ -425,6 +425,39 @@ func (c *fakeAuthClient) GetResources(ctx context.Context, req *proto.ListResour
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *fakeAuthClient) ListRemoteClusters(ctx context.Context, pageSize int, pageToken string) ([]types.RemoteCluster, string, error) {
|
||||
remoteClusters := make([]types.RemoteCluster, 0, len(c.clusterSpec.leafClusters))
|
||||
for leafClusterName := range c.clusterSpec.leafClusters {
|
||||
rc, err := types.NewRemoteCluster(leafClusterName)
|
||||
if err != nil {
|
||||
return nil, "", trace.Wrap(err)
|
||||
}
|
||||
remoteClusters = append(remoteClusters, rc)
|
||||
}
|
||||
return remoteClusters, "", nil
|
||||
}
|
||||
|
||||
func (c *fakeAuthClient) Ping(ctx context.Context) (proto.PingResponse, error) {
|
||||
return proto.PingResponse{
|
||||
ClusterName: c.clusterName,
|
||||
ProxyPublicAddr: c.clusterName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *fakeAuthClient) GetVnetConfig(ctx context.Context) (*vnet.VnetConfig, error) {
|
||||
vnetConfig := &vnet.VnetConfig{
|
||||
Spec: &vnet.VnetConfigSpec{
|
||||
Ipv4CidrRange: c.clusterSpec.cidrRange,
|
||||
},
|
||||
}
|
||||
for _, zone := range c.clusterSpec.customDNSZones {
|
||||
vnetConfig.Spec.CustomDnsZones = append(vnetConfig.Spec.CustomDnsZones, &vnet.CustomDNSZone{
|
||||
Suffix: zone,
|
||||
})
|
||||
}
|
||||
return vnetConfig, nil
|
||||
}
|
||||
|
||||
func TestDialFakeApp(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -460,13 +493,16 @@ func TestDialFakeApp(t *testing.T) {
|
||||
"leaf1.example.com": {
|
||||
apps: []string{"echo1.leaf1.example.com"},
|
||||
},
|
||||
"leaf2.example.com": {
|
||||
apps: []string{"echo1.leaf2.example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"root2.example.com": {
|
||||
apps: []string{"echo1.root2.example.com", "echo2.root2.example.com"},
|
||||
leafClusters: map[string]testClusterSpec{
|
||||
"leaf2.example.com": {
|
||||
apps: []string{"echo1.leaf2.example.com"},
|
||||
"leaf3.example.com": {
|
||||
apps: []string{"echo1.leaf3.example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -500,6 +536,14 @@ func TestDialFakeApp(t *testing.T) {
|
||||
app: "echo.nested.myzone.example.com",
|
||||
expectCIDR: "192.168.2.0/24",
|
||||
},
|
||||
{
|
||||
app: "echo1.leaf1.example.com",
|
||||
expectCIDR: defaultIPv4CIDRRange,
|
||||
},
|
||||
{
|
||||
app: "echo1.leaf2.example.com",
|
||||
expectCIDR: defaultIPv4CIDRRange,
|
||||
},
|
||||
{
|
||||
app: "echo1.root2.example.com",
|
||||
expectCIDR: defaultIPv4CIDRRange,
|
||||
@@ -508,6 +552,10 @@ func TestDialFakeApp(t *testing.T) {
|
||||
app: "echo2.root2.example.com",
|
||||
expectCIDR: defaultIPv4CIDRRange,
|
||||
},
|
||||
{
|
||||
app: "echo1.leaf3.example.com",
|
||||
expectCIDR: defaultIPv4CIDRRange,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
@@ -555,9 +603,6 @@ func TestDialFakeApp(t *testing.T) {
|
||||
"not.an.app.example.com.",
|
||||
"not.in.a.custom.zone",
|
||||
"in.an.invalid.zone",
|
||||
// Leaf clusters not supported yet.
|
||||
"echo1.leaf1.example.com.",
|
||||
"echo2.leaf1.example.com.",
|
||||
}
|
||||
for _, fqdn := range invalidTestCases {
|
||||
t.Run(fqdn, func(t *testing.T) {
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
vnetproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
"github.com/gravitational/teleport/lib/client/clientcache"
|
||||
@@ -116,16 +115,6 @@ func (p *vnetAppProvider) GetDialOptions(ctx context.Context, profileName string
|
||||
return dialOpts, nil
|
||||
}
|
||||
|
||||
func (p *vnetAppProvider) GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnetproto.VnetConfig, error) {
|
||||
clusterClient, err := p.clientCache.Get(ctx, profileName, leafClusterName)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
vnetConfigClient := clusterClient.AuthClient.VnetConfigServiceClient()
|
||||
vnetConfig, err := vnetConfigClient.GetVnetConfig(ctx, &vnetproto.GetVnetConfigRequest{})
|
||||
return vnetConfig, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// OnNewConnection gets called before each VNet connection. It's a noop as tsh doesn't need to do
|
||||
// anything extra here.
|
||||
func (p *vnetAppProvider) OnNewConnection(ctx context.Context, profileName, leafClusterName string, app types.Application) error {
|
||||
|
||||
Reference in New Issue
Block a user