| package bootstrap |
| |
| import ( |
| "context" |
| "crypto/x509" |
| "encoding/pem" |
| "net" |
| "os" |
| "sort" |
| "strings" |
| ) |
| |
| import ( |
| "github.com/pkg/errors" |
| |
| "google.golang.org/protobuf/proto" |
| ) |
| |
| import ( |
| mesh_proto "github.com/apache/dubbo-kubernetes/api/mesh/v1alpha1" |
| xds_config "github.com/apache/dubbo-kubernetes/pkg/config/xds" |
| bootstrap_config "github.com/apache/dubbo-kubernetes/pkg/config/xds/bootstrap" |
| core_mesh "github.com/apache/dubbo-kubernetes/pkg/core/resources/apis/mesh" |
| core_manager "github.com/apache/dubbo-kubernetes/pkg/core/resources/manager" |
| "github.com/apache/dubbo-kubernetes/pkg/core/resources/model/rest" |
| core_store "github.com/apache/dubbo-kubernetes/pkg/core/resources/store" |
| core_xds "github.com/apache/dubbo-kubernetes/pkg/core/xds" |
| "github.com/apache/dubbo-kubernetes/pkg/xds/bootstrap/types" |
| ) |
| |
| type BootstrapGenerator interface { |
| Generate(ctx context.Context, request types.BootstrapRequest) (proto.Message, DubboDpBootstrap, error) |
| } |
| |
| func NewDefaultBootstrapGenerator( |
| resManager core_manager.ResourceManager, |
| serverConfig *bootstrap_config.BootstrapServerConfig, |
| proxyConfig xds_config.Proxy, |
| dpServerCertFile string, |
| authEnabledForProxyType map[string]bool, |
| enableReloadableTokens bool, |
| hdsEnabled bool, |
| defaultAdminPort uint32, |
| ) (BootstrapGenerator, error) { |
| hostsAndIps, err := hostsAndIPs() |
| if err != nil { |
| return nil, err |
| } |
| if serverConfig.Params.XdsHost != "" && !hostsAndIps[serverConfig.Params.XdsHost] { |
| return nil, errors.Errorf("hostname: %s set by DUBBO_BOOTSTRAP_SERVER_PARAMS_XDS_HOST is not available in the DP Server certificate. Available hostnames: %q. Change the hostname or generate certificate with proper hostname.", serverConfig.Params.XdsHost, hostsAndIps.slice()) |
| } |
| return &bootstrapGenerator{ |
| resManager: resManager, |
| config: serverConfig, |
| proxyConfig: proxyConfig, |
| xdsCertFile: dpServerCertFile, |
| authEnabledForProxyType: authEnabledForProxyType, |
| enableReloadableTokens: enableReloadableTokens, |
| hostsAndIps: hostsAndIps, |
| hdsEnabled: hdsEnabled, |
| defaultAdminPort: defaultAdminPort, |
| }, nil |
| } |
| |
| func hostsAndIPs() (SANSet, error) { |
| hostsAndIps := map[string]bool{} |
| interfaces, err := net.Interfaces() |
| if err != nil { |
| return nil, err |
| } |
| for _, iface := range interfaces { |
| addrs, err := iface.Addrs() |
| if err != nil { |
| return nil, err |
| } |
| for _, addr := range addrs { |
| ipNet, ok := addr.(*net.IPNet) |
| if ok { |
| hostsAndIps[ipNet.IP.String()] = true |
| } |
| } |
| } |
| hostname, err := os.Hostname() |
| if err != nil { |
| return nil, err |
| } |
| hostsAndIps[hostname] = true |
| hostsAndIps["localhost"] = true |
| return hostsAndIps, nil |
| } |
| |
| type bootstrapGenerator struct { |
| resManager core_manager.ResourceManager |
| config *bootstrap_config.BootstrapServerConfig |
| proxyConfig xds_config.Proxy |
| authEnabledForProxyType map[string]bool |
| enableReloadableTokens bool |
| xdsCertFile string |
| hostsAndIps SANSet |
| hdsEnabled bool |
| defaultAdminPort uint32 |
| } |
| |
| type SANSet map[string]bool |
| |
| func (s SANSet) slice() []string { |
| sans := []string{} |
| for san := range s { |
| sans = append(sans, san) |
| } |
| sort.Strings(sans) |
| return sans |
| } |
| |
| func hostsAndIPsFromCertFile(dpServerCertFile string) (SANSet, error) { |
| certBytes, err := os.ReadFile(dpServerCertFile) |
| if err != nil { |
| return nil, errors.Wrap(err, "could not read certificate") |
| } |
| pemCert, _ := pem.Decode(certBytes) |
| if pemCert == nil { |
| return nil, errors.New("could not parse certificate") |
| } |
| cert, err := x509.ParseCertificate(pemCert.Bytes) |
| if err != nil { |
| return nil, errors.Wrap(err, "could not parse certificate") |
| } |
| |
| hostsAndIps := map[string]bool{} |
| for _, dnsName := range cert.DNSNames { |
| hostsAndIps[dnsName] = true |
| } |
| for _, ip := range cert.IPAddresses { |
| hostsAndIps[ip.String()] = true |
| } |
| return hostsAndIps, nil |
| } |
| |
| func (b *bootstrapGenerator) Generate(ctx context.Context, request types.BootstrapRequest) (proto.Message, DubboDpBootstrap, error) { |
| if request.ProxyType == "" { |
| request.ProxyType = string(mesh_proto.DataplaneProxyType) |
| } |
| dubboDpBootstrap := DubboDpBootstrap{} |
| //if err := b.validateRequest(request); err != nil { |
| // return nil, dubboDpBootstrap, err |
| //} |
| accessLogSocketPath := request.AccessLogSocketPath |
| if accessLogSocketPath == "" { |
| accessLogSocketPath = core_xds.AccessLogSocketName(os.TempDir(), request.Name, request.Mesh) |
| } |
| metricsSocketPath := request.MetricsResources.SocketPath |
| |
| if metricsSocketPath == "" { |
| metricsSocketPath = core_xds.MetricsHijackerSocketName(os.TempDir(), request.Name, request.Mesh) |
| } |
| |
| proxyId := core_xds.BuildProxyId(request.Mesh, request.Name) |
| params := configParameters{ |
| Id: proxyId.String(), |
| AdminAddress: b.config.Params.AdminAddress, |
| AdminAccessLogPath: b.adminAccessLogPath(request.OperatingSystem), |
| XdsHost: b.xdsHost(request), |
| XdsPort: b.config.Params.XdsPort, |
| XdsConnectTimeout: b.config.Params.XdsConnectTimeout.Duration, |
| DataplaneToken: request.DataplaneToken, |
| DataplaneTokenPath: request.DataplaneTokenPath, |
| DataplaneResource: request.DataplaneResource, |
| Version: &mesh_proto.Version{ |
| DubboDp: &mesh_proto.DubboDpVersion{ |
| Version: request.Version.DubboDp.Version, |
| GitTag: request.Version.DubboDp.GitTag, |
| GitCommit: request.Version.DubboDp.GitCommit, |
| BuildDate: request.Version.DubboDp.BuildDate, |
| }, |
| Envoy: &mesh_proto.EnvoyVersion{ |
| Version: request.Version.Envoy.Version, |
| Build: request.Version.Envoy.Build, |
| DubboDpCompatible: request.Version.Envoy.DubboDpCompatible, |
| }, |
| }, |
| DynamicMetadata: request.DynamicMetadata, |
| DNSPort: request.DNSPort, |
| EmptyDNSPort: request.EmptyDNSPort, |
| ProxyType: request.ProxyType, |
| Features: request.Features, |
| Resources: request.Resources, |
| Workdir: request.Workdir, |
| AccessLogSocketPath: accessLogSocketPath, |
| MetricsSocketPath: metricsSocketPath, |
| MetricsCertPath: request.MetricsResources.CertPath, |
| MetricsKeyPath: request.MetricsResources.KeyPath, |
| } |
| |
| setAdminPort := func(adminPortFromResource uint32) { |
| if adminPortFromResource != 0 { |
| params.AdminPort = adminPortFromResource |
| } else { |
| params.AdminPort = b.defaultAdminPort |
| } |
| } |
| |
| switch mesh_proto.ProxyType(params.ProxyType) { |
| case mesh_proto.IngressProxyType: |
| zoneIngress, err := b.zoneIngressFor(ctx, request, proxyId) |
| if err != nil { |
| return nil, dubboDpBootstrap, err |
| } |
| params.Service = "ingress" |
| setAdminPort(zoneIngress.Spec.GetNetworking().GetAdmin().GetPort()) |
| case mesh_proto.EgressProxyType: |
| zoneEgress, err := b.zoneEgressFor(ctx, request, proxyId) |
| if err != nil { |
| return nil, dubboDpBootstrap, err |
| } |
| params.Service = "egress" |
| setAdminPort(zoneEgress.Spec.GetNetworking().GetAdmin().GetPort()) |
| |
| default: |
| return nil, dubboDpBootstrap, errors.Errorf("unknown proxy type %v", params.ProxyType) |
| } |
| var err error |
| |
| config, err := genConfig(params, b.proxyConfig, b.enableReloadableTokens) |
| if err != nil { |
| return nil, dubboDpBootstrap, errors.Wrap(err, "failed creating bootstrap conf") |
| } |
| if err = config.Validate(); err != nil { |
| return nil, dubboDpBootstrap, errors.Wrap(err, "Envoy bootstrap config is not valid") |
| } |
| return config, dubboDpBootstrap, nil |
| } |
| |
| func (b *bootstrapGenerator) xdsHost(request types.BootstrapRequest) string { |
| if b.config.Params.XdsHost != "" { // XdsHost from config takes precedence over Host from request |
| return b.config.Params.XdsHost |
| } else { |
| return request.Host |
| } |
| } |
| |
| var DpTokenRequired = errors.New("Dataplane Token is required. Generate token using 'dubboctl generate dataplane-token > /path/file' and provide it via --dataplane-token-file=/path/file argument to Dubbo DP") |
| |
| var NotCA = errors.New("A data plane proxy is trying to verify the control plane using the certificate which is not a certificate authority (basic constraint 'CA' is set to 'false').\n" + |
| "Provide CA that was used to sign a certificate used in the control plane by using 'dubbo-dp run --ca-cert-file=file' or via DUBBO_CONTROL_PLANE_CA_CERT_FILE") |
| |
| func SANMismatchErr(host string, sans []string) error { |
| return errors.Errorf("A data plane proxy is trying to connect to the control plane using %q address, but the certificate in the control plane has the following SANs %q. "+ |
| "Either change the --cp-address in dubbo-dp to one of those or execute the following steps:\n"+ |
| "1) Generate a new certificate with the address you are trying to use. It is recommended to use trusted Certificate Authority, but you can also generate self-signed certificates using 'dubboctl generate tls-certificate --type=server --cp-hostname=%s'\n"+ |
| "2) Set DUBBO_GENERAL_TLS_CERT_FILE and DUBBO_GENERAL_TLS_KEY_FILE or the equivalent in Dubbo CP config file to the new certificate.\n"+ |
| "3) Restart the control plane to read the new certificate and start dubbo-dp.", host, sans, host) |
| } |
| |
| func ISSANMismatchErr(err error) bool { |
| if err == nil { |
| return false |
| } |
| return strings.HasPrefix(err.Error(), "A data plane proxy is trying to connect to the control plane using") |
| } |
| |
| // caCert gets CA cert that was used to signed cert that DP server is protected with. |
| // Technically result of this function does not have to be a valid CA. |
| // When user provides custom cert + key and does not provide --ca-cert-file to dubbo-dp run, this can return just a regular cert |
| |
| func (b *bootstrapGenerator) adminAccessLogPath(operatingSystem string) string { |
| if operatingSystem == "" { // backwards compatibility |
| return b.config.Params.AdminAccessLogPath |
| } |
| if b.config.Params.AdminAccessLogPath == os.DevNull && operatingSystem == "windows" { |
| // when AdminAccessLogPath was not explicitly set and DPP OS is Windows we need to set window specific DevNull. |
| // otherwise when CP is on Linux, we would set /dev/null which is not valid on Windows. |
| return "NUL" |
| } |
| return b.config.Params.AdminAccessLogPath |
| } |
| |
| func (b *bootstrapGenerator) validateRequest(request types.BootstrapRequest) error { |
| if b.authEnabledForProxyType[request.ProxyType] && request.DataplaneToken == "" && request.DataplaneTokenPath == "" { |
| return DpTokenRequired |
| } |
| if b.config.Params.XdsHost == "" { // XdsHost takes precedence over Host in the request, so validate only when it is not set |
| if !b.hostsAndIps[request.Host] { |
| return SANMismatchErr(request.Host, b.hostsAndIps.slice()) |
| } |
| } |
| return nil |
| } |
| |
| func (b *bootstrapGenerator) zoneEgressFor(ctx context.Context, request types.BootstrapRequest, proxyId *core_xds.ProxyId) (*core_mesh.ZoneEgressResource, error) { |
| if request.DataplaneResource != "" { |
| res, err := rest.YAML.UnmarshalCore([]byte(request.DataplaneResource)) |
| if err != nil { |
| return nil, err |
| } |
| zoneEgress, ok := res.(*core_mesh.ZoneEgressResource) |
| if !ok { |
| return nil, errors.Errorf("invalid resource") |
| } |
| if err := zoneEgress.Validate(); err != nil { |
| return nil, err |
| } |
| return zoneEgress, nil |
| } else { |
| zoneEgress := core_mesh.NewZoneEgressResource() |
| if err := b.resManager.Get(ctx, zoneEgress, core_store.GetBy(proxyId.ToResourceKey())); err != nil { |
| return nil, err |
| } |
| return zoneEgress, nil |
| } |
| } |
| |
| func (b *bootstrapGenerator) zoneIngressFor(ctx context.Context, request types.BootstrapRequest, proxyId *core_xds.ProxyId) (*core_mesh.ZoneIngressResource, error) { |
| if request.DataplaneResource != "" { |
| res, err := rest.YAML.UnmarshalCore([]byte(request.DataplaneResource)) |
| if err != nil { |
| return nil, err |
| } |
| zoneIngress, ok := res.(*core_mesh.ZoneIngressResource) |
| if !ok { |
| return nil, errors.Errorf("invalid resource") |
| } |
| if err := zoneIngress.Validate(); err != nil { |
| return nil, err |
| } |
| return zoneIngress, nil |
| } else { |
| zoneIngress := core_mesh.NewZoneIngressResource() |
| if err := b.resManager.Get(ctx, zoneIngress, core_store.GetBy(proxyId.ToResourceKey())); err != nil { |
| return nil, err |
| } |
| return zoneIngress, nil |
| } |
| } |