Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v16] [vnet] leaf cluster support #42747

Merged
merged 3 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,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{})
Expand Down
1 change: 1 addition & 0 deletions lib/auth/authclient/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,7 @@ type ClientI interface {
services.Integrations
services.KubeWaitingContainer
services.Notifications
services.VnetConfigGetter
types.Events

types.WebSessionsGetter
Expand Down
9 changes: 7 additions & 2 deletions lib/services/vnet_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 0 additions & 11 deletions lib/teleterm/vnet/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
139 changes: 87 additions & 52 deletions lib/vnet/app_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log/slog"
"net"
Expand All @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
var errNoMatch = errors.New("cluster does not match queried FQDN")

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 root cluster client, apps in this cluster will not be resolved.", "profile", profileName, "error", err)
return nil, errNoMatch
}

if isSubdomain(fqdn, profileName) {
// The queried app fqdn is a subdomain of the proxy address, this is a match.
return true, nil
// The queried app fqdn is direct subdomain of this cluster proxy address.
return rootClient, nil
}
// Not a proxy address subdomain, must check custom DNS zones.

// TODO(nklaassen): support leaf clusters.
vnetConfig, err := r.clusterConfigCache.getVnetConfig(ctx, profileName, "" /*leafClustername*/)
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 get VnetConfig, not checking custom DNS zones.", "profile", profileName, "error", err)
return false, nil
r.slog.ErrorContext(ctx, "Failed to list leaf clusters, apps in this cluster will not be resolved.", "profile", profileName, "error", err)
return nil, errNoMatch
}

// TODO(nklaassen): support leaf clusters.
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
}
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.
// 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
Expand All @@ -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,
Expand All @@ -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
}
Expand Down
Loading
Loading