// Package clienttransport builds the authenticated client side of the shared // Controller gRPC endpoint for Gateway and Checker processes. package clienttransport import ( "crypto/tls" "errors" "fmt" "net" "strconv" "strings" "proxy-pool/internal/config" "proxy-pool/internal/controlplane/tlsreload" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" ) var ErrInvalidTransport = errors.New("invalid control-plane client transport") // New creates a TLS 1.3 client transport for the supplied client role. The // caller supplies the role-specific certificate bundle, while the Controller // validates its SPIFFE role/ID on the peer certificate. func New( configuration config.ControlPlane, address string, clientTLS config.ClientTLS, override credentials.TransportCredentials, role string, ) (credentials.TransportCredentials, error) { if override != nil { return override, nil } if !ValidDialAddress(address) || strings.TrimSpace(role) != role || role == "" { return nil, ErrInvalidTransport } switch configuration.TLS.Mode { case "disabled": if !loopbackAddress(address) { return nil, fmt.Errorf("%w: plaintext control-plane target must be loopback", ErrInvalidTransport) } return insecure.NewCredentials(), nil case "mtls": if clientTLS.CertFile == "" || clientTLS.KeyFile == "" || clientTLS.ServerCAFile == "" { return nil, fmt.Errorf("%w: controlPlane.%s is required for mtls", ErrInvalidTransport, role) } certificate, err := tlsreload.NewCertificateProvider(clientTLS.CertFile, clientTLS.KeyFile) if err != nil { return nil, fmt.Errorf("load %s control-plane certificate: %w", role, err) } trust, err := tlsreload.NewTrustProvider(clientTLS.ServerCAFile) if err != nil { return nil, fmt.Errorf("load %s control-plane CA: %w", role, err) } host, _, _ := net.SplitHostPort(address) serverName := strings.Trim(host, "[]") return credentials.NewTLS(&tls.Config{ MinVersion: tls.VersionTLS13, GetClientCertificate: certificate.ClientCertificate, ServerName: serverName, // VerifyConnection retains standard verification semantics while allowing // the CA bundle to reload for the next control-plane handshake. InsecureSkipVerify: true, VerifyConnection: trust.VerifyServer(serverName), }), nil default: return nil, fmt.Errorf("%w: unsupported control-plane tls mode", ErrInvalidTransport) } } func ValidDialAddress(address string) bool { host, port, err := net.SplitHostPort(address) if err != nil || host == "" { return false } value, err := strconv.ParseUint(port, 10, 16) if err != nil || value == 0 { return false } parsed := net.ParseIP(strings.Trim(host, "[]")) return parsed == nil || !parsed.IsUnspecified() } func loopbackAddress(address string) bool { host, _, err := net.SplitHostPort(address) if err != nil { return false } host = strings.Trim(host, "[]") if strings.EqualFold(host, "localhost") { return true } ip := net.ParseIP(host) return ip != nil && ip.IsLoopback() }