transport: set default header list size to 8KB and guard behind an env variable. (#9019)

Part of: https://github.com/grpc/grpc-go/issues/2308

Follow up of PR: https://github.com/grpc/grpc-go/pull/8845 

This PR implements a change to restrict the default maximum header list
size to 8KB. The change is guarded by a new environment variable
GRPC_GO_EXPERIMENTAL_DEFAULT_HEADER_LIST_SIZE, which is disabled by
default.

When the environment variable is disabled (default), the legacy default
of 16MB is used. When enabled, RPCs with a total size of headers
exceeding 8KB will fail unless explicitly configured otherwise by the
user.

## Changes

`internal/envconfig`
* Added DefaultHeaderListSize: A boolean flag controlled by the
GRPC_GO_EXPERIMENTAL_DEFAULT_HEADER_LIST_SIZE environment variable.

`internal/transport/http2_client.go` and
`internal/transport/http2_server.go`
* Use upcomingDefaultHeaderListSize (8KB) as the default value if
`DefaultHeaderListSize` is enabled.
* If env variable is disabled and header size is greater than
upcomingDefaultHeaderListSize, log a warning.

RELEASE NOTES:
* transport: change default header list size from 16MB to 8KB and guard
it behind `GRPC_GO_EXPERIMENTAL_DEFAULT_HEADER_LIST_SIZE` env var which
is disabled by default. The env var will be enabled by default in
v1.82.0 release.
diff --git a/internal/envconfig/envconfig.go b/internal/envconfig/envconfig.go
index f6eb3b0..da15c37 100644
--- a/internal/envconfig/envconfig.go
+++ b/internal/envconfig/envconfig.go
@@ -126,6 +126,18 @@
 	// enabled by setting the env variable
 	// GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE to true.
 	EnablePriorityLBChildPolicyCache = boolFromEnv("GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE", false)
+
+	// Enable8KBDefaultHeaderListSize indicates that default maximum header list
+	// size is restricted to 8KB. This is disabled by default, but can be enabled
+	// by setting the environment variable
+	// "GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE" to "true".
+	// When disabled, the default maximum header list size of 16MB is used.
+	//
+	// When enabled, RPCs with a total size of headers exceeding 8KB will fail
+	// unless explicitly configured otherwise by the user.
+	//
+	// TODO: In release v1.82.0, env var will be enabled by default.
+	Enable8KBDefaultHeaderListSize = boolFromEnv("GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE", false)
 )
 
 func boolFromEnv(envVar string, def bool) bool {
diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go
index d6bc6a6..0f4f3ef 100644
--- a/internal/transport/http2_client.go
+++ b/internal/transport/http2_client.go
@@ -39,6 +39,7 @@
 	"google.golang.org/grpc/internal"
 	"google.golang.org/grpc/internal/channelz"
 	icredentials "google.golang.org/grpc/internal/credentials"
+	"google.golang.org/grpc/internal/envconfig"
 	"google.golang.org/grpc/internal/grpclog"
 	"google.golang.org/grpc/internal/grpcsync"
 	"google.golang.org/grpc/internal/grpcutil"
@@ -318,7 +319,13 @@
 	}
 	writeBufSize := opts.WriteBufferSize
 	readBufSize := opts.ReadBufferSize
+	// The default header list size is moving from 16MB to 8KB. The 8KB limit
+	// is only used if Enable8KBDefaultHeaderListSize is true; otherwise, the
+	// old 16MB default is used. User-specified options always take precedence.
 	maxHeaderListSize := defaultClientMaxHeaderListSize
+	if envconfig.Enable8KBDefaultHeaderListSize {
+		maxHeaderListSize = upcomingDefaultHeaderListSize
+	}
 	if opts.MaxHeaderListSize != nil {
 		maxHeaderListSize = *opts.MaxHeaderListSize
 	}
@@ -879,8 +886,8 @@
 				return false
 			}
 		}
-		if sz > int64(upcomingDefaultHeaderListSize) {
-			t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In a future release, this will be restricted to %d bytes.", sz, upcomingDefaultHeaderListSize, upcomingDefaultHeaderListSize)
+		if !envconfig.Enable8KBDefaultHeaderListSize && sz > int64(upcomingDefaultHeaderListSize) {
+			t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In release v1.82.0, GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE will be enabled by default, enforcing this limit.", sz, upcomingDefaultHeaderListSize)
 		}
 		return true
 	}
diff --git a/internal/transport/http2_server.go b/internal/transport/http2_server.go
index 3a8c36e..9d25bb3 100644
--- a/internal/transport/http2_server.go
+++ b/internal/transport/http2_server.go
@@ -38,6 +38,7 @@
 	"google.golang.org/protobuf/proto"
 
 	"google.golang.org/grpc/internal"
+	"google.golang.org/grpc/internal/envconfig"
 	"google.golang.org/grpc/internal/grpclog"
 	"google.golang.org/grpc/internal/grpcutil"
 	"google.golang.org/grpc/internal/pretty"
@@ -165,7 +166,13 @@
 	}
 	writeBufSize := config.WriteBufferSize
 	readBufSize := config.ReadBufferSize
+	// The default header list size is moving from 16MB to 8KB. The 8KB limit
+	// is only used if Enable8KBDefaultHeaderListSize is true; otherwise, the
+	// old 16MB default is used. User-specified options always take precedence.
 	maxHeaderListSize := defaultServerMaxHeaderListSize
+	if envconfig.Enable8KBDefaultHeaderListSize {
+		maxHeaderListSize = upcomingDefaultHeaderListSize
+	}
 	if config.MaxHeaderListSize != nil {
 		maxHeaderListSize = *config.MaxHeaderListSize
 	}
@@ -948,8 +955,8 @@
 			return false
 		}
 	}
-	if sz > int64(upcomingDefaultHeaderListSize) {
-		t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In a future release, this will be restricted to %d bytes.", sz, upcomingDefaultHeaderListSize, upcomingDefaultHeaderListSize)
+	if !envconfig.Enable8KBDefaultHeaderListSize && sz > int64(upcomingDefaultHeaderListSize) {
+		t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In release v1.82.0, GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE will be enabled by default, enforcing this limit.", sz, upcomingDefaultHeaderListSize)
 	}
 	return true
 }
diff --git a/test/end2end_test.go b/test/end2end_test.go
index e23ee5f..534c416 100644
--- a/test/end2end_test.go
+++ b/test/end2end_test.go
@@ -56,6 +56,7 @@
 	"google.golang.org/grpc/internal"
 	"google.golang.org/grpc/internal/binarylog"
 	"google.golang.org/grpc/internal/channelz"
+	"google.golang.org/grpc/internal/envconfig"
 	"google.golang.org/grpc/internal/grpcsync"
 	"google.golang.org/grpc/internal/grpctest"
 	"google.golang.org/grpc/internal/stubserver"
@@ -7211,3 +7212,97 @@
 		t.Fatalf("sh.delayedPickComplete count: %v, want: %v", got, want)
 	}
 }
+
+// TestEnable8KBDefaultHeaderListSize_ClientSendsLargeHeaders verifies that
+// when the Enable8KBDefaultHeaderListSize env var is enabled, and the client
+// sends metadata with a total size exceeding the 8KB default limit, the server
+// rejects the request and RPC will fail.
+func (s) TestEnable8KBDefaultHeaderListSize_ClientSendsLargeHeaders(t *testing.T) {
+	tests := []struct {
+		name     string
+		enable   bool
+		wantCode codes.Code
+	}{
+		{
+			name:     "env_var_enabled",
+			enable:   true,
+			wantCode: codes.Unavailable,
+		},
+		{
+			name:     "env_var_disabled",
+			enable:   false,
+			wantCode: codes.OK,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			testutils.SetEnvConfig(t, &envconfig.Enable8KBDefaultHeaderListSize, tc.enable)
+
+			ss := stubserver.StartTestService(t, nil)
+			if err := ss.StartClient(); err != nil {
+				t.Fatal("Failed to create client to stub server:", err)
+			}
+			defer ss.Stop()
+
+			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+			defer cancel()
+			md := metadata.MD{"large-key": []string{strings.Repeat("a", 9000)}}
+			ctx = metadata.NewOutgoingContext(ctx, md)
+
+			_, err := ss.Client.EmptyCall(ctx, &testpb.Empty{})
+			if got := status.Code(err); got != tc.wantCode {
+				t.Fatalf("EmptyCall() failed with code: %v, want: %v; full error: %v", got, tc.wantCode, err)
+			}
+		})
+	}
+}
+
+// TestEnable8KBDefaultHeaderListSize_ServerSendsLargeHeaders verifies that
+// when the Enable8KBDefaultHeaderListSize env var is enabled, and server
+// sends metadata with a total size exceeding the 8KB default limit, the RPC
+// will fail.
+func (s) TestEnable8KBDefaultHeaderListSize_ServerSendsLargeHeaders(t *testing.T) {
+	const largeHeaderSize = 9000
+	tests := []struct {
+		name     string
+		enable   bool
+		wantCode codes.Code
+	}{
+		{
+			name:     "env_var_enabled",
+			enable:   true,
+			wantCode: codes.Unavailable,
+		},
+		{
+			name:     "env_var_disabled",
+			enable:   false,
+			wantCode: codes.OK,
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			testutils.SetEnvConfig(t, &envconfig.Enable8KBDefaultHeaderListSize, tc.enable)
+
+			ss := &stubserver.StubServer{
+				EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
+					md := metadata.MD{"large-key": []string{strings.Repeat("a", largeHeaderSize)}}
+					grpc.SetHeader(ctx, md)
+					return &testpb.Empty{}, nil
+				},
+			}
+			if err := ss.Start(nil); err != nil {
+				t.Fatal("Error starting server:", err)
+			}
+			defer ss.Stop()
+
+			ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+			defer cancel()
+
+			_, err := ss.Client.EmptyCall(ctx, &testpb.Empty{})
+			if got := status.Code(err); got != tc.wantCode {
+				t.Fatalf("EmptyCall() failed with code: %v, want: %v; full error: %v", got, tc.wantCode, err)
+			}
+		})
+	}
+}