// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package pbpeering import ( "crypto/tls" "crypto/x509" "errors" "fmt" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/proto/private/pbcommon" ) // RequestDatacenter implements structs.RPCInfo func (req *GenerateTokenRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // RequestDatacenter implements structs.RPCInfo func (req *EstablishRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // RequestDatacenter implements structs.RPCInfo func (req *PeeringReadRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // RequestDatacenter implements structs.RPCInfo func (req *PeeringListRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // RequestDatacenter implements structs.RPCInfo func (req *PeeringWriteRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // RequestDatacenter implements structs.RPCInfo func (req *PeeringDeleteRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // RequestDatacenter implements structs.RPCInfo func (req *TrustBundleReadRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // RequestDatacenter implements structs.RPCInfo func (req *TrustBundleListByServiceRequest) RequestDatacenter() string { // Cross-datacenter requests are not allowed for peering actions because // they rely on WAN-federation. return "" } // ShouldDial returns true when the peering was stored via the peering initiation endpoint, // AND the peering is not marked as terminated by our peer. // If we generated a token for this peer we did not store our server addresses under PeerServerAddresses or ManualServerAddresses. // These server addresses are for dialing, and only the peer initiating the peering will do the dialing. func (p *Peering) ShouldDial() bool { return len(p.PeerServerAddresses) > 0 || len(p.ManualServerAddresses) > 0 } // GetAddressesToDial returns the listing of addresses that should be dialed for the peering. // It will ensure that manual addresses take precedence, if any are defined. func (p *Peering) GetAddressesToDial() []string { if len(p.ManualServerAddresses) > 0 { return p.ManualServerAddresses } return p.PeerServerAddresses } func (x PeeringState) GoString() string { return x.String() } // ConcatenatedRootPEMs concatenates and returns all PEM-encoded public certificates // in a peer's trust bundle. func (b *PeeringTrustBundle) ConcatenatedRootPEMs() string { if b == nil { return "" } var rootPEMs string for _, pem := range b.RootPEMs { rootPEMs += lib.EnsureTrailingNewline(pem) } return rootPEMs } // enumcover:PeeringState func PeeringStateToAPI(s PeeringState) api.PeeringState { switch s { case PeeringState_PENDING: return api.PeeringStatePending case PeeringState_ESTABLISHING: return api.PeeringStateEstablishing case PeeringState_ACTIVE: return api.PeeringStateActive case PeeringState_FAILING: return api.PeeringStateFailing case PeeringState_DELETING: return api.PeeringStateDeleting case PeeringState_TERMINATED: return api.PeeringStateTerminated case PeeringState_UNDEFINED: fallthrough default: return api.PeeringStateUndefined } } // enumcover:api.PeeringState func PeeringStateFromAPI(t api.PeeringState) PeeringState { switch t { case api.PeeringStatePending: return PeeringState_PENDING case api.PeeringStateEstablishing: return PeeringState_ESTABLISHING case api.PeeringStateActive: return PeeringState_ACTIVE case api.PeeringStateFailing: return PeeringState_FAILING case api.PeeringStateDeleting: return PeeringState_DELETING case api.PeeringStateTerminated: return PeeringState_TERMINATED case api.PeeringStateUndefined: fallthrough default: return PeeringState_UNDEFINED } } func StreamStatusToAPI(status *StreamStatus) api.PeeringStreamStatus { return api.PeeringStreamStatus{ ImportedServices: status.ImportedServices, ExportedServices: status.ExportedServices, LastHeartbeat: TimePtrFromProto(status.LastHeartbeat), LastReceive: TimePtrFromProto(status.LastReceive), LastSend: TimePtrFromProto(status.LastSend), } } func StreamStatusFromAPI(status api.PeeringStreamStatus) *StreamStatus { return &StreamStatus{ ImportedServices: status.ImportedServices, ExportedServices: status.ExportedServices, LastHeartbeat: TimePtrToProto(status.LastHeartbeat), LastReceive: TimePtrToProto(status.LastReceive), LastSend: TimePtrToProto(status.LastSend), } } func (p *Peering) IsActive() bool { if p == nil || p.State == PeeringState_TERMINATED { return false } if p.DeletedAt == nil { return true } // The minimum protobuf timestamp is the Unix epoch rather than go's zero. return structs.IsZeroProtoTime(p.DeletedAt) } // Validate is a validation helper that checks whether a secret ID is embedded in the container type. func (s *SecretsWriteRequest) Validate() error { if s.PeerID == "" { return errors.New("missing peer ID") } switch r := s.Request.(type) { case *SecretsWriteRequest_GenerateToken: if r != nil && r.GenerateToken.GetEstablishmentSecret() != "" { return nil } case *SecretsWriteRequest_Establish: if r != nil && r.Establish.GetActiveStreamSecret() != "" { return nil } case *SecretsWriteRequest_ExchangeSecret: if r != nil && r.ExchangeSecret.GetPendingStreamSecret() != "" { return nil } case *SecretsWriteRequest_PromotePending: if r != nil && r.PromotePending.GetActiveStreamSecret() != "" { return nil } default: return fmt.Errorf("unexpected request type %T", s.Request) } return errors.New("missing secret ID") } // TLSDialOption returns the gRPC DialOption to secure the transport if CAPems // ara available. If no CAPems were provided in the peering token then the // WithInsecure dial option is returned. func (p *Peering) TLSDialOption() (grpc.DialOption, error) { //nolint:staticcheck tlsOption := grpc.WithInsecure() if len(p.PeerCAPems) > 0 { var haveCerts bool pool := x509.NewCertPool() for _, pem := range p.PeerCAPems { if !pool.AppendCertsFromPEM([]byte(pem)) { return nil, fmt.Errorf("failed to parse PEM %s", pem) } if len(pem) > 0 { haveCerts = true } } if !haveCerts { return nil, fmt.Errorf("failed to build cert pool from peer CA pems") } cfg := tls.Config{ ServerName: p.PeerServerName, RootCAs: pool, } tlsOption = grpc.WithTransportCredentials(credentials.NewTLS(&cfg)) } return tlsOption, nil } func (p *Peering) ToAPI() *api.Peering { var t api.Peering PeeringToAPI(p, &t) return &t } // TODO consider using mog for this func (resp *PeeringListResponse) ToAPI() []*api.Peering { list := make([]*api.Peering, len(resp.Peerings)) for i, p := range resp.Peerings { list[i] = p.ToAPI() } return list } // TODO consider using mog for this func (resp *GenerateTokenResponse) ToAPI() *api.PeeringGenerateTokenResponse { var t api.PeeringGenerateTokenResponse GenerateTokenResponseToAPI(resp, &t) return &t } // TODO consider using mog for this func (resp *EstablishResponse) ToAPI() *api.PeeringEstablishResponse { var t api.PeeringEstablishResponse EstablishResponseToAPI(resp, &t) return &t } func (r *RemoteInfo) IsEmpty() bool { if r == nil { return true } return r.Partition == "" && r.Datacenter == "" && r.Locality.IsEmpty() } // convenience func NewGenerateTokenRequestFromAPI(req *api.PeeringGenerateTokenRequest) *GenerateTokenRequest { if req == nil { return nil } t := &GenerateTokenRequest{} GenerateTokenRequestFromAPI(req, t) return t } // convenience func NewEstablishRequestFromAPI(req *api.PeeringEstablishRequest) *EstablishRequest { if req == nil { return nil } t := &EstablishRequest{} EstablishRequestFromAPI(req, t) return t } func TimePtrFromProto(s *timestamppb.Timestamp) *time.Time { if s == nil { return nil } t := s.AsTime() return &t } func TimePtrToProto(s *time.Time) *timestamppb.Timestamp { if s == nil { return nil } return timestamppb.New(*s) } // DeepCopy returns a copy of the PeeringTrustBundle that can be passed around // without worrying about the receiver unsafely modifying it. It is used by the // generated DeepCopy methods in proxycfg. func (o *PeeringTrustBundle) DeepCopy() *PeeringTrustBundle { cp, ok := proto.Clone(o).(*PeeringTrustBundle) if !ok { panic(fmt.Sprintf("failed to clone *PeeringTrustBundle, got: %T", cp)) } return cp } // TODO: handle this with mog // LocalityToStructs converts a protobuf Locality to a struct Locality. func LocalityToStructs(l *pbcommon.Locality) *structs.Locality { if l == nil { return nil } return &structs.Locality{ Region: l.Region, Zone: l.Zone, } } // TODO: handle this with mog // LocalityFromStructs converts a struct Locality to a protobuf Locality. func LocalityFromStructs(l *structs.Locality) *pbcommon.Locality { if l == nil { return nil } return &pbcommon.Locality{ Region: l.Region, Zone: l.Zone, } } // TODO: handle this with mog // LocalityToAPI converts a protobuf Locality to an API Locality. func LocalityToAPI(l *pbcommon.Locality) *api.Locality { if l == nil { return nil } return &api.Locality{ Region: l.Region, Zone: l.Zone, } } // TODO: handle this with mog // LocalityFromProto converts an API Locality to a protobuf Locality. func LocalityFromAPI(l *api.Locality) *pbcommon.Locality { if l == nil { return nil } return &pbcommon.Locality{ Region: l.Region, Zone: l.Zone, } }