| /* |
| * |
| * 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 |
| } |