| /* | |
| * | |
| * Copyright 2023 Google LLC | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * https://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| * | |
| */ | |
| // Package fallback provides default implementations of fallback options when S2A fails. | |
| package fallback | |
| import ( | |
| "context" | |
| "crypto/tls" | |
| "fmt" | |
| "net" | |
| "google.golang.org/grpc/credentials" | |
| "google.golang.org/grpc/grpclog" | |
| ) | |
| const ( | |
| alpnProtoStrH2 = "h2" | |
| alpnProtoStrHTTP = "http/1.1" | |
| defaultHTTPSPort = "443" | |
| ) | |
| // FallbackTLSConfigGRPC is a tls.Config used by the DefaultFallbackClientHandshakeFunc function. | |
| // It supports GRPC use case, thus the alpn is set to 'h2'. | |
| var FallbackTLSConfigGRPC = tls.Config{ | |
| MinVersion: tls.VersionTLS13, | |
| ClientSessionCache: nil, | |
| NextProtos: []string{alpnProtoStrH2}, | |
| } | |
| // FallbackTLSConfigHTTP is a tls.Config used by the DefaultFallbackDialerAndAddress func. | |
| // It supports the HTTP use case and the alpn is set to both 'http/1.1' and 'h2'. | |
| var FallbackTLSConfigHTTP = tls.Config{ | |
| MinVersion: tls.VersionTLS13, | |
| ClientSessionCache: nil, | |
| NextProtos: []string{alpnProtoStrH2, alpnProtoStrHTTP}, | |
| } | |
| // ClientHandshake establishes a TLS connection and returns it, plus its auth info. | |
| // Inputs: | |
| // | |
| // targetServer: the server attempted with S2A. | |
| // conn: the tcp connection to the server at address targetServer that was passed into S2A's ClientHandshake func. | |
| // If fallback is successful, the `conn` should be closed. | |
| // err: the error encountered when performing the client-side TLS handshake with S2A. | |
| type ClientHandshake func(ctx context.Context, targetServer string, conn net.Conn, err error) (net.Conn, credentials.AuthInfo, error) | |
| // DefaultFallbackClientHandshakeFunc returns a ClientHandshake function, | |
| // which establishes a TLS connection to the provided fallbackAddr, returns the new connection and its auth info. | |
| // Example use: | |
| // | |
| // transportCreds, _ = s2a.NewClientCreds(&s2a.ClientOptions{ | |
| // S2AAddress: s2aAddress, | |
| // FallbackOpts: &s2a.FallbackOptions{ // optional | |
| // FallbackClientHandshakeFunc: fallback.DefaultFallbackClientHandshakeFunc(fallbackAddr), | |
| // }, | |
| // }) | |
| // | |
| // The fallback server's certificate must be verifiable using OS root store. | |
| // The fallbackAddr is expected to be a network address, e.g. example.com:port. If port is not specified, | |
| // it uses default port 443. | |
| // In the returned function's TLS config, ClientSessionCache is explicitly set to nil to disable TLS resumption, | |
| // and min TLS version is set to 1.3. | |
| func DefaultFallbackClientHandshakeFunc(fallbackAddr string) (ClientHandshake, error) { | |
| var fallbackDialer = tls.Dialer{Config: &FallbackTLSConfigGRPC} | |
| return defaultFallbackClientHandshakeFuncInternal(fallbackAddr, fallbackDialer.DialContext) | |
| } | |
| func defaultFallbackClientHandshakeFuncInternal(fallbackAddr string, dialContextFunc func(context.Context, string, string) (net.Conn, error)) (ClientHandshake, error) { | |
| fallbackServerAddr, err := processFallbackAddr(fallbackAddr) | |
| if err != nil { | |
| if grpclog.V(1) { | |
| grpclog.Infof("error processing fallback address [%s]: %v", fallbackAddr, err) | |
| } | |
| return nil, err | |
| } | |
| return func(ctx context.Context, targetServer string, conn net.Conn, s2aErr error) (net.Conn, credentials.AuthInfo, error) { | |
| fbConn, fbErr := dialContextFunc(ctx, "tcp", fallbackServerAddr) | |
| if fbErr != nil { | |
| grpclog.Infof("dialing to fallback server %s failed: %v", fallbackServerAddr, fbErr) | |
| return nil, nil, fmt.Errorf("dialing to fallback server %s failed: %v; S2A client handshake with %s error: %w", fallbackServerAddr, fbErr, targetServer, s2aErr) | |
| } | |
| tc, success := fbConn.(*tls.Conn) | |
| if !success { | |
| grpclog.Infof("the connection with fallback server is expected to be tls but isn't") | |
| return nil, nil, fmt.Errorf("the connection with fallback server is expected to be tls but isn't; S2A client handshake with %s error: %w", targetServer, s2aErr) | |
| } | |
| tlsInfo := credentials.TLSInfo{ | |
| State: tc.ConnectionState(), | |
| CommonAuthInfo: credentials.CommonAuthInfo{ | |
| SecurityLevel: credentials.PrivacyAndIntegrity, | |
| }, | |
| } | |
| if grpclog.V(1) { | |
| grpclog.Infof("ConnectionState.NegotiatedProtocol: %v", tc.ConnectionState().NegotiatedProtocol) | |
| grpclog.Infof("ConnectionState.HandshakeComplete: %v", tc.ConnectionState().HandshakeComplete) | |
| grpclog.Infof("ConnectionState.ServerName: %v", tc.ConnectionState().ServerName) | |
| } | |
| conn.Close() | |
| return fbConn, tlsInfo, nil | |
| }, nil | |
| } | |
| // DefaultFallbackDialerAndAddress returns a TLS dialer and the network address to dial. | |
| // Example use: | |
| // | |
| // fallbackDialer, fallbackServerAddr := fallback.DefaultFallbackDialerAndAddress(fallbackAddr) | |
| // dialTLSContext := s2a.NewS2aDialTLSContextFunc(&s2a.ClientOptions{ | |
| // S2AAddress: s2aAddress, // required | |
| // FallbackOpts: &s2a.FallbackOptions{ | |
| // FallbackDialer: &s2a.FallbackDialer{ | |
| // Dialer: fallbackDialer, | |
| // ServerAddr: fallbackServerAddr, | |
| // }, | |
| // }, | |
| // }) | |
| // | |
| // The fallback server's certificate should be verifiable using OS root store. | |
| // The fallbackAddr is expected to be a network address, e.g. example.com:port. If port is not specified, | |
| // it uses default port 443. | |
| // In the returned function's TLS config, ClientSessionCache is explicitly set to nil to disable TLS resumption, | |
| // and min TLS version is set to 1.3. | |
| func DefaultFallbackDialerAndAddress(fallbackAddr string) (*tls.Dialer, string, error) { | |
| fallbackServerAddr, err := processFallbackAddr(fallbackAddr) | |
| if err != nil { | |
| if grpclog.V(1) { | |
| grpclog.Infof("error processing fallback address [%s]: %v", fallbackAddr, err) | |
| } | |
| return nil, "", err | |
| } | |
| return &tls.Dialer{Config: &FallbackTLSConfigHTTP}, fallbackServerAddr, nil | |
| } | |
| func processFallbackAddr(fallbackAddr string) (string, error) { | |
| var fallbackServerAddr string | |
| var err error | |
| if fallbackAddr == "" { | |
| return "", fmt.Errorf("empty fallback address") | |
| } | |
| _, _, err = net.SplitHostPort(fallbackAddr) | |
| if err != nil { | |
| // fallbackAddr does not have port suffix | |
| fallbackServerAddr = net.JoinHostPort(fallbackAddr, defaultHTTPSPort) | |
| } else { | |
| // FallbackServerAddr already has port suffix | |
| fallbackServerAddr = fallbackAddr | |
| } | |
| return fallbackServerAddr, nil | |
| } |