diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2bf75b9cdb2..c7ca8122e2d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -26818,6 +26818,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_cluster_end2end_test.cc
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_cluster_end2end_test PUBLIC cxx_std_14)
@@ -27085,6 +27086,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_cluster_type_end2end_test.cc
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_cluster_type_end2end_test PUBLIC cxx_std_14)
@@ -27324,6 +27326,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_core_end2end_test.cc
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_core_end2end_test PUBLIC cxx_std_14)
@@ -27589,6 +27592,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_csds_end2end_test.cc
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_csds_end2end_test PUBLIC cxx_std_14)
@@ -27773,6 +27777,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test.cc
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_end2end_test PUBLIC cxx_std_14)
@@ -28000,6 +28005,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_fault_injection_end2end_test.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_fault_injection_end2end_test PUBLIC cxx_std_14)
@@ -28743,6 +28749,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_outlier_detection_end2end_test.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_outlier_detection_end2end_test PUBLIC cxx_std_14)
@@ -28918,6 +28925,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_override_host_end2end_test.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_override_host_end2end_test PUBLIC cxx_std_14)
@@ -29163,6 +29171,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_pick_first_end2end_test.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_pick_first_end2end_test PUBLIC cxx_std_14)
@@ -29331,6 +29340,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_ring_hash_end2end_test.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_ring_hash_end2end_test PUBLIC cxx_std_14)
@@ -29503,6 +29513,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_rls_end2end_test.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_rls_end2end_test PUBLIC cxx_std_14)
@@ -29786,6 +29797,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_routing_end2end_test.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/util/tls_test_utils.cc
   )
   target_compile_features(xds_routing_end2end_test PUBLIC cxx_std_14)
@@ -30024,6 +30036,7 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_MAC OR _gRPC_PLATFORM_POSIX)
     test/cpp/end2end/test_service_impl.cc
     test/cpp/end2end/xds/xds_end2end_test_lib.cc
     test/cpp/end2end/xds/xds_server.cc
+    test/cpp/end2end/xds/xds_utils.cc
     test/cpp/end2end/xds/xds_wrr_end2end_test.cc
     test/cpp/util/tls_test_utils.cc
   )
diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml
index 750debd3f6b..41d37ec8ee3 100644
--- a/build_autogenerated.yaml
+++ b/build_autogenerated.yaml
@@ -17560,6 +17560,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -17598,6 +17599,7 @@ targets:
   - test/cpp/end2end/xds/xds_cluster_end2end_test.cc
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -17646,6 +17648,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -17685,6 +17688,7 @@ targets:
   - test/cpp/end2end/xds/xds_cluster_type_end2end_test.cc
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -17737,6 +17741,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -17774,6 +17779,7 @@ targets:
   - test/cpp/end2end/xds/xds_core_end2end_test.cc
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -17840,6 +17846,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -17880,6 +17887,7 @@ targets:
   - test/cpp/end2end/xds/xds_csds_end2end_test.cc
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -17900,6 +17908,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -17943,6 +17952,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test.cc
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -17981,6 +17991,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -18020,6 +18031,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_fault_injection_end2end_test.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -18259,6 +18271,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -18298,6 +18311,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_outlier_detection_end2end_test.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -18316,6 +18330,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -18356,6 +18371,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_override_host_end2end_test.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -18404,6 +18420,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -18444,6 +18461,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_pick_first_end2end_test.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -18463,6 +18481,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -18502,6 +18521,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_ring_hash_end2end_test.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -18521,6 +18541,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/lookup/v1/rls.proto
@@ -18561,6 +18582,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_rls_end2end_test.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -18622,6 +18644,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -18661,6 +18684,7 @@ targets:
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_routing_end2end_test.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
   - gtest
@@ -18706,6 +18730,7 @@ targets:
   - test/cpp/end2end/test_service_impl.h
   - test/cpp/end2end/xds/xds_end2end_test_lib.h
   - test/cpp/end2end/xds/xds_server.h
+  - test/cpp/end2end/xds/xds_utils.h
   - test/cpp/util/tls_test_utils.h
   src:
   - src/proto/grpc/testing/duplicate/echo_duplicate.proto
@@ -18744,6 +18769,7 @@ targets:
   - test/cpp/end2end/test_service_impl.cc
   - test/cpp/end2end/xds/xds_end2end_test_lib.cc
   - test/cpp/end2end/xds/xds_server.cc
+  - test/cpp/end2end/xds/xds_utils.cc
   - test/cpp/end2end/xds/xds_wrr_end2end_test.cc
   - test/cpp/util/tls_test_utils.cc
   deps:
diff --git a/test/cpp/end2end/xds/BUILD b/test/cpp/end2end/xds/BUILD
index a1feea3a974..8cc655b9f69 100644
--- a/test/cpp/end2end/xds/BUILD
+++ b/test/cpp/end2end/xds/BUILD
@@ -45,6 +45,24 @@ grpc_cc_library(
     ],
 )
 
+grpc_cc_library(
+    name = "xds_utils",
+    testonly = True,
+    srcs = ["xds_utils.cc"],
+    hdrs = ["xds_utils.h"],
+    deps = [
+        ":xds_server",
+        "//src/proto/grpc/testing/xds/v3:cluster_proto",
+        "//src/proto/grpc/testing/xds/v3:discovery_proto",
+        "//src/proto/grpc/testing/xds/v3:endpoint_proto",
+        "//src/proto/grpc/testing/xds/v3:http_connection_manager_proto",
+        "//src/proto/grpc/testing/xds/v3:listener_proto",
+        "//src/proto/grpc/testing/xds/v3:route_proto",
+        "//src/proto/grpc/testing/xds/v3:router_proto",
+        "//test/core/util:grpc_test_util_base",
+    ],
+)
+
 grpc_cc_library(
     name = "xds_end2end_test_lib",
     testonly = True,
@@ -55,6 +73,7 @@ grpc_cc_library(
     ],
     deps = [
         ":xds_server",
+        ":xds_utils",
         "//:gpr",
         "//:grpc",
         "//:grpc++",
diff --git a/test/cpp/end2end/xds/xds_cluster_end2end_test.cc b/test/cpp/end2end/xds/xds_cluster_end2end_test.cc
index a658f319819..c43d8fbc2a0 100644
--- a/test/cpp/end2end/xds/xds_cluster_end2end_test.cc
+++ b/test/cpp/end2end/xds/xds_cluster_end2end_test.cc
@@ -343,7 +343,7 @@ TEST_P(CdsDeletionTest, ClusterDeleted) {
 
 // Tests that we ignore Cluster deletions if configured to do so.
 TEST_P(CdsDeletionTest, ClusterDeletionIgnored) {
-  InitClient(BootstrapBuilder().SetIgnoreResourceDeletion());
+  InitClient(XdsBootstrapBuilder().SetIgnoreResourceDeletion());
   CreateAndStartBackends(2);
   // Bring up client pointing to backend 0 and wait for it to connect.
   EdsResourceArgs args({{"locality0", CreateEndpointsForBackends(0, 1)}});
diff --git a/test/cpp/end2end/xds/xds_core_end2end_test.cc b/test/cpp/end2end/xds/xds_core_end2end_test.cc
index 1142b2f0216..a6a9896752e 100644
--- a/test/cpp/end2end/xds/xds_core_end2end_test.cc
+++ b/test/cpp/end2end/xds/xds_core_end2end_test.cc
@@ -308,7 +308,7 @@ TEST_P(GlobalXdsClientTest, InvalidListenerStillExistsIfPreviouslyCached) {
 class TimeoutTest : public XdsEnd2endTest {
  protected:
   void SetUp() override {
-    InitClient(BootstrapBuilder(), /*lb_expected_authority=*/"",
+    InitClient(XdsBootstrapBuilder(), /*lb_expected_authority=*/"",
                /*xds_resource_does_not_exist_timeout_ms=*/2000);
   }
 };
@@ -628,7 +628,7 @@ TEST_P(XdsFederationTest, FederationTargetNoAuthorityWithResourceTemplate) {
   const char* kNewClusterName =
       "xdstp://xds.example.com/envoy.config.cluster.v3.Cluster/"
       "new_cluster_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.SetClientDefaultListenerResourceNameTemplate(kNewListenerTemplate);
   builder.AddAuthority(
       kAuthority, absl::StrCat("localhost:", authority_balancer_->port()),
@@ -682,7 +682,7 @@ TEST_P(XdsFederationTest, FederationTargetAuthorityDefaultResourceTemplate) {
   const char* kNewClusterName =
       "xdstp://xds.example.com/envoy.config.cluster.v3.Cluster/"
       "cluster_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.AddAuthority(kAuthority,
                        absl::StrCat("localhost:", authority_balancer_->port()));
   InitClient(builder);
@@ -751,7 +751,7 @@ TEST_P(XdsFederationTest, FederationTargetAuthorityWithResourceTemplate) {
   const char* kNewClusterName =
       "xdstp://xds.example.com/envoy.config.cluster.v3.Cluster/"
       "cluster_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.AddAuthority(kAuthority,
                        absl::StrCat("localhost:", authority_balancer_->port()),
                        kNewListenerTemplate);
@@ -807,7 +807,7 @@ TEST_P(XdsFederationTest, TargetUriAuthorityUnknown) {
   const char* kNewListenerTemplate =
       "xdstp://xds.example.com/envoy.config.listener.v3.Listener/"
       "client/%s?psm_project_id=1234";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.AddAuthority(
       kAuthority, absl::StrCat("localhost:", grpc_pick_unused_port_or_die()),
       kNewListenerTemplate);
@@ -838,7 +838,7 @@ TEST_P(XdsFederationTest, RdsResourceNameAuthorityUnknown) {
   const char* kNewRouteConfigName =
       "xdstp://xds.unknown.com/envoy.config.route.v3.RouteConfiguration/"
       "new_route_config_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.AddAuthority(kAuthority,
                        absl::StrCat("localhost:", authority_balancer_->port()),
                        kNewListenerTemplate);
@@ -884,7 +884,7 @@ TEST_P(XdsFederationTest, CdsResourceNameAuthorityUnknown) {
   const char* kNewClusterName =
       "xdstp://xds.unknown.com/envoy.config.cluster.v3.Cluster/"
       "cluster_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.AddAuthority(kAuthority,
                        absl::StrCat("localhost:", authority_balancer_->port()),
                        kNewListenerTemplate);
@@ -937,7 +937,7 @@ TEST_P(XdsFederationTest, EdsResourceNameAuthorityUnknown) {
   const char* kNewClusterName =
       "xdstp://xds.example.com/envoy.config.cluster.v3.Cluster/"
       "cluster_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.AddAuthority(kAuthority,
                        absl::StrCat("localhost:", authority_balancer_->port()),
                        kNewListenerTemplate);
@@ -1004,7 +1004,7 @@ TEST_P(XdsFederationTest, FederationServer) {
   const char* kNewClusterName =
       "xdstp://xds.example.com/envoy.config.cluster.v3.Cluster/"
       "new_cluster_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.SetClientDefaultListenerResourceNameTemplate(kNewListenerTemplate);
   builder.SetServerListenerResourceNameTemplate(kNewServerListenerTemplate);
   builder.AddAuthority(
@@ -1150,7 +1150,7 @@ TEST_P(XdsFederationLoadReportingTest, FederationMultipleLoadReportingTest) {
       "cluster_name";
   const size_t kNumRpcsToDefaultBalancer = 5;
   const size_t kNumRpcsToAuthorityBalancer = 10;
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   builder.AddAuthority(kAuthority,
                        absl::StrCat("localhost:", authority_balancer_->port()),
                        kNewListenerTemplate);
@@ -1261,7 +1261,7 @@ TEST_P(XdsFederationLoadReportingTest, SameServerInAuthorityAndTopLevel) {
   const char* kNewEdsServiceName =
       "xdstp://xds.example.com/envoy.config.endpoint.v3.ClusterLoadAssignment/"
       "edsservice_name";
-  BootstrapBuilder builder = BootstrapBuilder();
+  XdsBootstrapBuilder builder;
   std::string xds_server =
       absl::StrCat("localhost:", authority_balancer_->port());
   builder.AddAuthority(kAuthority, xds_server);
@@ -1334,7 +1334,7 @@ INSTANTIATE_TEST_SUITE_P(XdsTest, SecureNamingTest,
 
 // Tests that secure naming check passes if target name is expected.
 TEST_P(SecureNamingTest, TargetNameIsExpected) {
-  InitClient(BootstrapBuilder(), /*lb_expected_authority=*/"localhost:%d");
+  InitClient(XdsBootstrapBuilder(), /*lb_expected_authority=*/"localhost:%d");
   CreateAndStartBackends(4);
   EdsResourceArgs args({
       {"locality0", CreateEndpointsForBackends()},
@@ -1346,7 +1346,7 @@ TEST_P(SecureNamingTest, TargetNameIsExpected) {
 // Tests that secure naming check fails if target name is unexpected.
 TEST_P(SecureNamingTest, TargetNameIsUnexpected) {
   GTEST_FLAG_SET(death_test_style, "threadsafe");
-  InitClient(BootstrapBuilder(),
+  InitClient(XdsBootstrapBuilder(),
              /*lb_expected_authority=*/"incorrect_server_name");
   CreateAndStartBackends(4);
   EdsResourceArgs args({
diff --git a/test/cpp/end2end/xds/xds_csds_end2end_test.cc b/test/cpp/end2end/xds/xds_csds_end2end_test.cc
index c616dfcc4d1..c99bf4bfcac 100644
--- a/test/cpp/end2end/xds/xds_csds_end2end_test.cc
+++ b/test/cpp/end2end/xds/xds_csds_end2end_test.cc
@@ -619,7 +619,7 @@ class CsdsShortAdsTimeoutTest : public ClientStatusDiscoveryServiceTest {
  protected:
   void SetUp() override {
     // Shorten the ADS subscription timeout to speed up the test run.
-    InitClient(BootstrapBuilder(), /*lb_expected_authority=*/"",
+    InitClient(XdsBootstrapBuilder(), /*lb_expected_authority=*/"",
                /*xds_resource_does_not_exist_timeout_ms=*/2000);
   }
 };
diff --git a/test/cpp/end2end/xds/xds_end2end_test.cc b/test/cpp/end2end/xds/xds_end2end_test.cc
index 9eb8380fcfa..f4e6e1cdbf8 100644
--- a/test/cpp/end2end/xds/xds_end2end_test.cc
+++ b/test/cpp/end2end/xds/xds_end2end_test.cc
@@ -282,7 +282,7 @@ FakeCertificateProvider::CertDataMapWrapper* g_fake2_cert_data_map = nullptr;
 class XdsSecurityTest : public XdsEnd2endTest {
  protected:
   void SetUp() override {
-    BootstrapBuilder builder = BootstrapBuilder();
+    XdsBootstrapBuilder builder;
     builder.AddCertificateProviderPlugin("fake_plugin1", "fake1");
     builder.AddCertificateProviderPlugin("fake_plugin2", "fake2");
     std::vector<std::string> fields;
@@ -734,7 +734,7 @@ class XdsEnabledServerTest : public XdsEnd2endTest {
  protected:
   void SetUp() override {}  // No-op -- individual tests do this themselves.
 
-  void DoSetUp(BootstrapBuilder builder = BootstrapBuilder()) {
+  void DoSetUp(XdsBootstrapBuilder builder = XdsBootstrapBuilder()) {
     InitClient(builder);
     CreateBackends(1, /*xds_enabled=*/true);
     EdsResourceArgs args({{"locality0", CreateEndpointsForBackends(0, 1)}});
@@ -749,7 +749,7 @@ TEST_P(XdsEnabledServerTest, Basic) {
 }
 
 TEST_P(XdsEnabledServerTest, ListenerDeletionIgnored) {
-  DoSetUp(BootstrapBuilder().SetIgnoreResourceDeletion());
+  DoSetUp(XdsBootstrapBuilder().SetIgnoreResourceDeletion());
   backends_[0]->Start();
   WaitForBackend(DEBUG_LOCATION, 0);
   // Check that we ACKed.
@@ -843,7 +843,7 @@ TEST_P(XdsEnabledServerTest, ListenerAddressMismatch) {
 class XdsServerSecurityTest : public XdsEnd2endTest {
  protected:
   void SetUp() override {
-    BootstrapBuilder builder = BootstrapBuilder();
+    XdsBootstrapBuilder builder;
     builder.AddCertificateProviderPlugin("fake_plugin1", "fake1");
     builder.AddCertificateProviderPlugin("fake_plugin2", "fake2");
     std::vector<std::string> fields;
diff --git a/test/cpp/end2end/xds/xds_end2end_test_lib.cc b/test/cpp/end2end/xds/xds_end2end_test_lib.cc
index b24a813c566..b2c893b93a6 100644
--- a/test/cpp/end2end/xds/xds_end2end_test_lib.cc
+++ b/test/cpp/end2end/xds/xds_end2end_test_lib.cc
@@ -52,10 +52,6 @@ namespace grpc {
 namespace testing {
 
 using ::envoy::config::core::v3::HealthStatus;
-using ::envoy::config::endpoint::v3::ClusterLoadAssignment;
-using ::envoy::config::listener::v3::Listener;
-using ::envoy::extensions::filters::network::http_connection_manager::v3::
-    HttpConnectionManager;
 
 using ::grpc::experimental::ExternalCertificateVerifier;
 using ::grpc::experimental::IdentityKeyCertPair;
@@ -300,112 +296,6 @@ void XdsEnd2endTest::BalancerServerThread::ShutdownAllServices() {
   lrs_service_->Shutdown();
 }
 
-//
-// XdsEnd2endTest::BootstrapBuilder
-//
-
-std::string XdsEnd2endTest::BootstrapBuilder::Build() {
-  std::vector<std::string> fields;
-  fields.push_back(MakeXdsServersText(top_server_));
-  if (!client_default_listener_resource_name_template_.empty()) {
-    fields.push_back(
-        absl::StrCat("  \"client_default_listener_resource_name_template\": \"",
-                     client_default_listener_resource_name_template_, "\""));
-  }
-  fields.push_back(MakeNodeText());
-  if (!server_listener_resource_name_template_.empty()) {
-    fields.push_back(
-        absl::StrCat("  \"server_listener_resource_name_template\": \"",
-                     server_listener_resource_name_template_, "\""));
-  }
-  fields.push_back(MakeCertificateProviderText());
-  fields.push_back(MakeAuthorityText());
-  return absl::StrCat("{", absl::StrJoin(fields, ",\n"), "}");
-}
-
-std::string XdsEnd2endTest::BootstrapBuilder::MakeXdsServersText(
-    absl::string_view server_uri) {
-  constexpr char kXdsServerTemplate[] =
-      "      \"xds_servers\": [\n"
-      "        {\n"
-      "          \"server_uri\": \"<SERVER_URI>\",\n"
-      "          \"channel_creds\": [\n"
-      "            {\n"
-      "              \"type\": \"fake\"\n"
-      "            }\n"
-      "          ],\n"
-      "          \"server_features\": [<SERVER_FEATURES>]\n"
-      "        }\n"
-      "      ]";
-  std::vector<std::string> server_features;
-  if (ignore_resource_deletion_) {
-    server_features.push_back("\"ignore_resource_deletion\"");
-  }
-  return absl::StrReplaceAll(
-      kXdsServerTemplate,
-      {{"<SERVER_URI>", server_uri},
-       {"<SERVER_FEATURES>", absl::StrJoin(server_features, ", ")}});
-}
-
-std::string XdsEnd2endTest::BootstrapBuilder::MakeNodeText() {
-  constexpr char kXdsNode[] =
-      "  \"node\": {\n"
-      "    \"id\": \"xds_end2end_test\",\n"
-      "    \"cluster\": \"test\",\n"
-      "    \"metadata\": {\n"
-      "      \"foo\": \"bar\"\n"
-      "    },\n"
-      "    \"locality\": {\n"
-      "      \"region\": \"corp\",\n"
-      "      \"zone\": \"svl\",\n"
-      "      \"sub_zone\": \"mp3\"\n"
-      "    }\n"
-      "  }";
-  return kXdsNode;
-}
-
-std::string XdsEnd2endTest::BootstrapBuilder::MakeCertificateProviderText() {
-  std::vector<std::string> entries;
-  for (const auto& p : plugins_) {
-    const std::string& key = p.first;
-    const PluginInfo& plugin_info = p.second;
-    std::vector<std::string> fields;
-    fields.push_back(absl::StrFormat("    \"%s\": {", key));
-    if (!plugin_info.plugin_config.empty()) {
-      fields.push_back(
-          absl::StrFormat("      \"plugin_name\": \"%s\",", plugin_info.name));
-      fields.push_back(absl::StrCat("      \"config\": {\n",
-                                    plugin_info.plugin_config, "\n      }"));
-    } else {
-      fields.push_back(
-          absl::StrFormat("      \"plugin_name\": \"%s\"", plugin_info.name));
-    }
-    fields.push_back("    }");
-    entries.push_back(absl::StrJoin(fields, "\n"));
-  }
-  return absl::StrCat("  \"certificate_providers\": {\n",
-                      absl::StrJoin(entries, ",\n"), "  \n}");
-}
-
-std::string XdsEnd2endTest::BootstrapBuilder::MakeAuthorityText() {
-  std::vector<std::string> entries;
-  for (const auto& p : authorities_) {
-    const std::string& name = p.first;
-    const AuthorityInfo& authority_info = p.second;
-    std::vector<std::string> fields = {
-        MakeXdsServersText(authority_info.server)};
-    if (!authority_info.client_listener_resource_name_template.empty()) {
-      fields.push_back(absl::StrCat(
-          "\"client_listener_resource_name_template\": \"",
-          authority_info.client_listener_resource_name_template, "\""));
-    }
-    entries.push_back(absl::StrCat(absl::StrFormat("\"%s\": {\n  ", name),
-                                   absl::StrJoin(fields, ",\n"), "\n}"));
-  }
-  return absl::StrCat("\"authorities\": {\n", absl::StrJoin(entries, ",\n"),
-                      "\n}");
-}
-
 //
 // XdsEnd2endTest::RpcOptions
 //
@@ -443,18 +333,6 @@ void XdsEnd2endTest::RpcOptions::SetupRpc(ClientContext* context,
 // XdsEnd2endTest
 //
 
-const char XdsEnd2endTest::kDefaultLocalityRegion[] =
-    "xds_default_locality_region";
-const char XdsEnd2endTest::kDefaultLocalityZone[] = "xds_default_locality_zone";
-
-const char XdsEnd2endTest::kServerName[] = "server.example.com";
-const char XdsEnd2endTest::kDefaultRouteConfigurationName[] =
-    "route_config_name";
-const char XdsEnd2endTest::kDefaultClusterName[] = "cluster_name";
-const char XdsEnd2endTest::kDefaultEdsServiceName[] = "eds_service_name";
-const char XdsEnd2endTest::kDefaultServerRouteConfigurationName[] =
-    "default_server_route_config_name";
-
 const char XdsEnd2endTest::kCaCertPath[] = "src/core/tsi/test_creds/ca.pem";
 const char XdsEnd2endTest::kServerCertPath[] =
     "src/core/tsi/test_creds/server1.pem";
@@ -464,30 +342,10 @@ const char XdsEnd2endTest::kServerKeyPath[] =
 const char XdsEnd2endTest::kRequestMessage[] = "Live long and prosper.";
 
 XdsEnd2endTest::XdsEnd2endTest() : balancer_(CreateAndStartBalancer()) {
-  // Initialize default xDS resources.
-  // Construct LDS resource.
-  default_listener_.set_name(kServerName);
-  HttpConnectionManager http_connection_manager;
-  auto* filter = http_connection_manager.add_http_filters();
-  filter->set_name("router");
-  filter->mutable_typed_config()->PackFrom(
-      envoy::extensions::filters::http::router::v3::Router());
-  default_listener_.mutable_api_listener()->mutable_api_listener()->PackFrom(
-      http_connection_manager);
-  // Construct RDS resource.
-  default_route_config_.set_name(kDefaultRouteConfigurationName);
-  auto* virtual_host = default_route_config_.add_virtual_hosts();
-  virtual_host->add_domains("*");
-  auto* route = virtual_host->add_routes();
-  route->mutable_match()->set_prefix("");
-  route->mutable_route()->set_cluster(kDefaultClusterName);
-  // Construct CDS resource.
-  default_cluster_.set_name(kDefaultClusterName);
-  default_cluster_.set_type(Cluster::EDS);
-  auto* eds_config = default_cluster_.mutable_eds_cluster_config();
-  eds_config->mutable_eds_config()->mutable_self();
-  eds_config->set_service_name(kDefaultEdsServiceName);
-  default_cluster_.set_lb_policy(Cluster::ROUND_ROBIN);
+  // Initialize default client-side xDS resources.
+  default_listener_ = XdsResourceUtils::DefaultListener();
+  default_route_config_ = XdsResourceUtils::DefaultRouteConfig();
+  default_cluster_ = XdsResourceUtils::DefaultCluster();
   if (GetParam().enable_load_reporting()) {
     default_cluster_.mutable_lrs_server()->mutable_self();
   }
@@ -495,21 +353,9 @@ XdsEnd2endTest::XdsEnd2endTest() : balancer_(CreateAndStartBalancer()) {
   SetListenerAndRouteConfiguration(balancer_.get(), default_listener_,
                                    default_route_config_);
   balancer_->ads_service()->SetCdsResource(default_cluster_);
-  // Construct a default server-side RDS resource for tests to use.
-  default_server_route_config_.set_name(kDefaultServerRouteConfigurationName);
-  virtual_host = default_server_route_config_.add_virtual_hosts();
-  virtual_host->add_domains("*");
-  route = virtual_host->add_routes();
-  route->mutable_match()->set_prefix("");
-  route->mutable_non_forwarding_action();
-  // Construct a default server-side Listener resource
-  default_server_listener_.mutable_address()
-      ->mutable_socket_address()
-      ->set_address(grpc_core::LocalIp());
-  default_server_listener_.mutable_default_filter_chain()
-      ->add_filters()
-      ->mutable_typed_config()
-      ->PackFrom(http_connection_manager);
+  // Initialize default server-side xDS resources.
+  default_server_route_config_ = XdsResourceUtils::DefaultServerRouteConfig();
+  default_server_listener_ = XdsResourceUtils::DefaultServerListener();
 }
 
 void XdsEnd2endTest::TearDown() {
@@ -534,83 +380,6 @@ XdsEnd2endTest::CreateAndStartBalancer() {
   return balancer;
 }
 
-std::string XdsEnd2endTest::GetServerListenerName(int port) {
-  return absl::StrCat("grpc/server?xds.resource.listening_address=",
-                      grpc_core::LocalIp(), ":", port);
-}
-
-Listener XdsEnd2endTest::PopulateServerListenerNameAndPort(
-    const Listener& listener_template, int port) {
-  Listener listener = listener_template;
-  listener.set_name(GetServerListenerName(port));
-  listener.mutable_address()->mutable_socket_address()->set_port_value(port);
-  return listener;
-}
-
-HttpConnectionManager XdsEnd2endTest::ClientHcmAccessor::Unpack(
-    const Listener& listener) const {
-  HttpConnectionManager http_connection_manager;
-  listener.api_listener().api_listener().UnpackTo(&http_connection_manager);
-  return http_connection_manager;
-}
-
-void XdsEnd2endTest::ClientHcmAccessor::Pack(const HttpConnectionManager& hcm,
-                                             Listener* listener) const {
-  auto* api_listener = listener->mutable_api_listener()->mutable_api_listener();
-  api_listener->PackFrom(hcm);
-}
-
-HttpConnectionManager XdsEnd2endTest::ServerHcmAccessor::Unpack(
-    const Listener& listener) const {
-  HttpConnectionManager http_connection_manager;
-  listener.default_filter_chain().filters().at(0).typed_config().UnpackTo(
-      &http_connection_manager);
-  return http_connection_manager;
-}
-
-void XdsEnd2endTest::ServerHcmAccessor::Pack(const HttpConnectionManager& hcm,
-                                             Listener* listener) const {
-  listener->mutable_default_filter_chain()
-      ->mutable_filters()
-      ->at(0)
-      .mutable_typed_config()
-      ->PackFrom(hcm);
-}
-
-void XdsEnd2endTest::SetListenerAndRouteConfiguration(
-    BalancerServerThread* balancer, Listener listener,
-    const RouteConfiguration& route_config, const HcmAccessor& hcm_accessor) {
-  HttpConnectionManager http_connection_manager = hcm_accessor.Unpack(listener);
-  if (GetParam().enable_rds_testing()) {
-    auto* rds = http_connection_manager.mutable_rds();
-    rds->set_route_config_name(route_config.name());
-    rds->mutable_config_source()->mutable_self();
-    balancer->ads_service()->SetRdsResource(route_config);
-  } else {
-    *http_connection_manager.mutable_route_config() = route_config;
-  }
-  hcm_accessor.Pack(http_connection_manager, &listener);
-  balancer->ads_service()->SetLdsResource(listener);
-}
-
-void XdsEnd2endTest::SetRouteConfiguration(
-    BalancerServerThread* balancer, const RouteConfiguration& route_config,
-    const Listener* listener_to_copy) {
-  if (GetParam().enable_rds_testing()) {
-    balancer->ads_service()->SetRdsResource(route_config);
-  } else {
-    Listener listener(listener_to_copy == nullptr ? default_listener_
-                                                  : *listener_to_copy);
-    HttpConnectionManager http_connection_manager;
-    listener.mutable_api_listener()->mutable_api_listener()->UnpackTo(
-        &http_connection_manager);
-    *(http_connection_manager.mutable_route_config()) = route_config;
-    listener.mutable_api_listener()->mutable_api_listener()->PackFrom(
-        http_connection_manager);
-    balancer->ads_service()->SetLdsResource(listener);
-  }
-}
-
 std::vector<XdsEnd2endTest::EdsResourceArgs::Endpoint>
 XdsEnd2endTest::CreateEndpointsForBackends(size_t start_index,
                                            size_t stop_index,
@@ -624,57 +393,6 @@ XdsEnd2endTest::CreateEndpointsForBackends(size_t start_index,
   return endpoints;
 }
 
-ClusterLoadAssignment XdsEnd2endTest::BuildEdsResource(
-    const EdsResourceArgs& args, absl::string_view eds_service_name) {
-  ClusterLoadAssignment assignment;
-  assignment.set_cluster_name(eds_service_name);
-  for (const auto& locality : args.locality_list) {
-    auto* endpoints = assignment.add_endpoints();
-    endpoints->mutable_load_balancing_weight()->set_value(locality.lb_weight);
-    endpoints->set_priority(locality.priority);
-    endpoints->mutable_locality()->set_region(kDefaultLocalityRegion);
-    endpoints->mutable_locality()->set_zone(kDefaultLocalityZone);
-    endpoints->mutable_locality()->set_sub_zone(locality.sub_zone);
-    for (size_t i = 0; i < locality.endpoints.size(); ++i) {
-      const auto& endpoint = locality.endpoints[i];
-      auto* lb_endpoints = endpoints->add_lb_endpoints();
-      if (locality.endpoints.size() > i &&
-          locality.endpoints[i].health_status != HealthStatus::UNKNOWN) {
-        lb_endpoints->set_health_status(endpoint.health_status);
-      }
-      if (locality.endpoints.size() > i && endpoint.lb_weight >= 1) {
-        lb_endpoints->mutable_load_balancing_weight()->set_value(
-            endpoint.lb_weight);
-      }
-      auto* endpoint_proto = lb_endpoints->mutable_endpoint();
-      auto* socket_address =
-          endpoint_proto->mutable_address()->mutable_socket_address();
-      socket_address->set_address(grpc_core::LocalIp());
-      socket_address->set_port_value(endpoint.port);
-      for (int port : endpoint.additional_ports) {
-        socket_address = endpoint_proto->add_additional_addresses()
-                             ->mutable_address()
-                             ->mutable_socket_address();
-        socket_address->set_address(grpc_core::LocalIp());
-        socket_address->set_port_value(port);
-      }
-    }
-  }
-  if (!args.drop_categories.empty()) {
-    auto* policy = assignment.mutable_policy();
-    for (const auto& p : args.drop_categories) {
-      const std::string& name = p.first;
-      const uint32_t parts_per_million = p.second;
-      auto* drop_overload = policy->add_drop_overloads();
-      drop_overload->set_category(name);
-      auto* drop_percentage = drop_overload->mutable_drop_percentage();
-      drop_percentage->set_numerator(parts_per_million);
-      drop_percentage->set_denominator(args.drop_denominator);
-    }
-  }
-  return assignment;
-}
-
 void XdsEnd2endTest::ResetBackendCounters(size_t start_index,
                                           size_t stop_index) {
   if (stop_index == 0) stop_index = backends_.size();
@@ -728,7 +446,7 @@ std::vector<int> XdsEnd2endTest::GetBackendPorts(size_t start_index,
   return backend_ports;
 }
 
-void XdsEnd2endTest::InitClient(BootstrapBuilder builder,
+void XdsEnd2endTest::InitClient(XdsBootstrapBuilder builder,
                                 std::string lb_expected_authority,
                                 int xds_resource_does_not_exist_timeout_ms) {
   if (xds_resource_does_not_exist_timeout_ms > 0) {
diff --git a/test/cpp/end2end/xds/xds_end2end_test_lib.h b/test/cpp/end2end/xds/xds_end2end_test_lib.h
index 34dec7d8821..fb83b901803 100644
--- a/test/cpp/end2end/xds/xds_end2end_test_lib.h
+++ b/test/cpp/end2end/xds/xds_end2end_test_lib.h
@@ -50,6 +50,7 @@
 #include "test/cpp/end2end/counted_service.h"
 #include "test/cpp/end2end/test_service_impl.h"
 #include "test/cpp/end2end/xds/xds_server.h"
+#include "test/cpp/end2end/xds/xds_utils.h"
 
 namespace grpc {
 namespace testing {
@@ -201,29 +202,9 @@ class XdsTestType {
 // the indexes in the range [start_index, stop_index).  If stop_index
 // is 0, backends_.size() is used.  Backends may or may not be
 // xDS-enabled, at the discretion of the test.
-class XdsEnd2endTest : public ::testing::TestWithParam<XdsTestType> {
+class XdsEnd2endTest : public ::testing::TestWithParam<XdsTestType>,
+                       public XdsResourceUtils {
  protected:
-  using Cluster = ::envoy::config::cluster::v3::Cluster;
-  using ClusterLoadAssignment =
-      ::envoy::config::endpoint::v3::ClusterLoadAssignment;
-  using Listener = ::envoy::config::listener::v3::Listener;
-  using RouteConfiguration = ::envoy::config::route::v3::RouteConfiguration;
-  using HttpConnectionManager = ::envoy::extensions::filters::network::
-      http_connection_manager::v3::HttpConnectionManager;
-
-  // Default values for locality fields.
-  static const char kDefaultLocalityRegion[];
-  static const char kDefaultLocalityZone[];
-  static const uint32_t kDefaultLocalityWeight = 3;
-  static const int kDefaultLocalityPriority = 0;
-
-  // Default resource names.
-  static const char kServerName[];
-  static const char kDefaultRouteConfigurationName[];
-  static const char kDefaultClusterName[];
-  static const char kDefaultEdsServiceName[];
-  static const char kDefaultServerRouteConfigurationName[];
-
   // TLS certificate paths.
   static const char kCaCertPath[];
   static const char kServerCertPath[];
@@ -430,73 +411,6 @@ class XdsEnd2endTest : public ::testing::TestWithParam<XdsTestType> {
     std::shared_ptr<LrsServiceImpl> lrs_service_;
   };
 
-  // A builder for the xDS bootstrap config.
-  class BootstrapBuilder {
-   public:
-    BootstrapBuilder() {}
-    BootstrapBuilder& SetIgnoreResourceDeletion() {
-      ignore_resource_deletion_ = true;
-      return *this;
-    }
-    // If ignore_if_set is true, sets the default server only if it has
-    // not already been set.
-    BootstrapBuilder& SetDefaultServer(const std::string& server,
-                                       bool ignore_if_set = false) {
-      if (!ignore_if_set || top_server_.empty()) top_server_ = server;
-      return *this;
-    }
-    BootstrapBuilder& SetClientDefaultListenerResourceNameTemplate(
-        const std::string& client_default_listener_resource_name_template) {
-      client_default_listener_resource_name_template_ =
-          client_default_listener_resource_name_template;
-      return *this;
-    }
-    BootstrapBuilder& AddCertificateProviderPlugin(
-        const std::string& key, const std::string& name,
-        const std::string& plugin_config = "") {
-      plugins_[key] = {name, plugin_config};
-      return *this;
-    }
-    BootstrapBuilder& AddAuthority(
-        const std::string& authority, const std::string& server = "",
-        const std::string& client_listener_resource_name_template = "") {
-      authorities_[authority] = {server,
-                                 client_listener_resource_name_template};
-      return *this;
-    }
-    BootstrapBuilder& SetServerListenerResourceNameTemplate(
-        const std::string& server_listener_resource_name_template = "") {
-      server_listener_resource_name_template_ =
-          server_listener_resource_name_template;
-      return *this;
-    }
-
-    std::string Build();
-
-   private:
-    struct PluginInfo {
-      std::string name;
-      std::string plugin_config;
-    };
-    struct AuthorityInfo {
-      std::string server;
-      std::string client_listener_resource_name_template;
-    };
-
-    std::string MakeXdsServersText(absl::string_view server_uri);
-    std::string MakeNodeText();
-    std::string MakeCertificateProviderText();
-    std::string MakeAuthorityText();
-
-    bool ignore_resource_deletion_ = false;
-    std::string top_server_;
-    std::string client_default_listener_resource_name_template_;
-    std::map<std::string /*key*/, PluginInfo> plugins_;
-    std::map<std::string /*authority_name*/, AuthorityInfo> authorities_;
-    std::string server_listener_resource_name_template_ =
-        "grpc/server?xds.resource.listening_address=%s";
-  };
-
   // RPC services used to talk to the backends.
   enum RpcService {
     SERVICE_ECHO,
@@ -525,47 +439,17 @@ class XdsEnd2endTest : public ::testing::TestWithParam<XdsTestType> {
   // balancer_, which is already populated with default resources.
   std::unique_ptr<BalancerServerThread> CreateAndStartBalancer();
 
-  // Returns the name of the server-side xDS Listener resource for a
-  // backend on the specified port.
-  std::string GetServerListenerName(int port);
-
-  // Returns a copy of listener_template with the server-side resource
-  // name and the port in the socket address populated.
-  Listener PopulateServerListenerNameAndPort(const Listener& listener_template,
-                                             int port);
-
-  // Interface for accessing HttpConnectionManager config in Listener.
-  class HcmAccessor {
-   public:
-    virtual ~HcmAccessor() = default;
-    virtual HttpConnectionManager Unpack(const Listener& listener) const = 0;
-    virtual void Pack(const HttpConnectionManager& hcm,
-                      Listener* listener) const = 0;
-  };
-
-  // Client-side impl.
-  class ClientHcmAccessor : public HcmAccessor {
-   public:
-    HttpConnectionManager Unpack(const Listener& listener) const override;
-    void Pack(const HttpConnectionManager& hcm,
-              Listener* listener) const override;
-  };
-
-  // Server-side impl.
-  class ServerHcmAccessor : public HcmAccessor {
-   public:
-    HttpConnectionManager Unpack(const Listener& listener) const override;
-    void Pack(const HttpConnectionManager& hcm,
-              Listener* listener) const override;
-  };
-
   // Sets the Listener and RouteConfiguration resource on the specified
   // balancer.  If RDS is in use, they will be set as separate resources;
   // otherwise, the RouteConfig will be inlined into the Listener.
   void SetListenerAndRouteConfiguration(
       BalancerServerThread* balancer, Listener listener,
       const RouteConfiguration& route_config,
-      const HcmAccessor& hcm_accessor = ClientHcmAccessor());
+      const HcmAccessor& hcm_accessor = ClientHcmAccessor()) {
+    XdsResourceUtils::SetListenerAndRouteConfiguration(
+        balancer->ads_service(), std::move(listener), route_config,
+        GetParam().enable_rds_testing(), hcm_accessor);
+  }
 
   // A convenient wrapper for setting the Listener and
   // RouteConfiguration resources on the server side.
@@ -583,53 +467,11 @@ class XdsEnd2endTest : public ::testing::TestWithParam<XdsTestType> {
   // (either listener_to_copy, or if that is null, default_listener_).
   void SetRouteConfiguration(BalancerServerThread* balancer,
                              const RouteConfiguration& route_config,
-                             const Listener* listener_to_copy = nullptr);
-
-  // Arguments for constructing an EDS resource.
-  struct EdsResourceArgs {
-    // An individual endpoint for a backend running on a specified port.
-    struct Endpoint {
-      explicit Endpoint(int port,
-                        ::envoy::config::core::v3::HealthStatus health_status =
-                            ::envoy::config::core::v3::HealthStatus::UNKNOWN,
-                        int lb_weight = 1,
-                        std::vector<int> additional_ports = {})
-          : port(port),
-            health_status(health_status),
-            lb_weight(lb_weight),
-            additional_ports(std::move(additional_ports)) {}
-
-      int port;
-      ::envoy::config::core::v3::HealthStatus health_status;
-      int lb_weight;
-      std::vector<int> additional_ports;
-    };
-
-    // A locality.
-    struct Locality {
-      Locality(std::string sub_zone, std::vector<Endpoint> endpoints,
-               uint32_t lb_weight = kDefaultLocalityWeight,
-               int priority = kDefaultLocalityPriority)
-          : sub_zone(std::move(sub_zone)),
-            endpoints(std::move(endpoints)),
-            lb_weight(lb_weight),
-            priority(priority) {}
-
-      const std::string sub_zone;
-      std::vector<Endpoint> endpoints;
-      uint32_t lb_weight;
-      int priority;
-    };
-
-    EdsResourceArgs() = default;
-    explicit EdsResourceArgs(std::vector<Locality> locality_list)
-        : locality_list(std::move(locality_list)) {}
-
-    std::vector<Locality> locality_list;
-    std::map<std::string, uint32_t> drop_categories;
-    ::envoy::type::v3::FractionalPercent::DenominatorType drop_denominator =
-        ::envoy::type::v3::FractionalPercent::MILLION;
-  };
+                             const Listener* listener_to_copy = nullptr) {
+    XdsResourceUtils::SetRouteConfiguration(
+        balancer->ads_service(), route_config, GetParam().enable_rds_testing(),
+        listener_to_copy);
+  }
 
   // Helper method for generating an endpoint for a backend, for use in
   // constructing an EDS resource.
@@ -662,11 +504,6 @@ class XdsEnd2endTest : public ::testing::TestWithParam<XdsTestType> {
     return EdsResourceArgs::Endpoint(grpc_pick_unused_port_or_die());
   }
 
-  // Constructs an EDS resource.
-  ClusterLoadAssignment BuildEdsResource(
-      const EdsResourceArgs& args,
-      absl::string_view eds_service_name = kDefaultEdsServiceName);
-
   //
   // Backend management
   //
@@ -727,7 +564,7 @@ class XdsEnd2endTest : public ::testing::TestWithParam<XdsTestType> {
   // Initializes global state for the client, such as the bootstrap file
   // and channel args for the XdsClient.  Then calls ResetStub().
   // All tests must call this exactly once at the start of the test.
-  void InitClient(BootstrapBuilder builder = BootstrapBuilder(),
+  void InitClient(XdsBootstrapBuilder builder = XdsBootstrapBuilder(),
                   std::string lb_expected_authority = "",
                   int xds_resource_does_not_exist_timeout_ms = 0);
 
diff --git a/test/cpp/end2end/xds/xds_routing_end2end_test.cc b/test/cpp/end2end/xds/xds_routing_end2end_test.cc
index a4012732404..bb42c8cbcd7 100644
--- a/test/cpp/end2end/xds/xds_routing_end2end_test.cc
+++ b/test/cpp/end2end/xds/xds_routing_end2end_test.cc
@@ -115,7 +115,7 @@ TEST_P(LdsDeletionTest, ListenerDeleted) {
 
 // Tests that we ignore Listener deletions if configured to do so.
 TEST_P(LdsDeletionTest, ListenerDeletionIgnored) {
-  InitClient(BootstrapBuilder().SetIgnoreResourceDeletion());
+  InitClient(XdsBootstrapBuilder().SetIgnoreResourceDeletion());
   CreateAndStartBackends(2);
   // Bring up client pointing to backend 0 and wait for it to connect.
   EdsResourceArgs args({{"locality0", CreateEndpointsForBackends(0, 1)}});
diff --git a/test/cpp/end2end/xds/xds_utils.cc b/test/cpp/end2end/xds/xds_utils.cc
new file mode 100644
index 00000000000..8ad0cea3c82
--- /dev/null
+++ b/test/cpp/end2end/xds/xds_utils.cc
@@ -0,0 +1,375 @@
+// Copyright 2017 gRPC authors.
+//
+// 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
+//
+//     http://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.
+//
+
+#include "test/cpp/end2end/xds/xds_utils.h"
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "absl/memory/memory.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/str_replace.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+
+#include <grpcpp/security/tls_certificate_provider.h>
+
+#include "src/core/ext/filters/http/server/http_server_filter.h"
+#include "src/core/ext/xds/xds_channel_args.h"
+#include "src/core/ext/xds/xds_client_grpc.h"
+#include "src/core/lib/gpr/tmpfile.h"
+#include "src/core/lib/gprpp/env.h"
+#include "src/core/lib/iomgr/load_file.h"
+#include "src/core/lib/surface/server.h"
+#include "src/cpp/client/secure_credentials.h"
+#include "src/proto/grpc/testing/xds/v3/router.grpc.pb.h"
+#include "test/core/util/resolve_localhost_ip46.h"
+
+namespace grpc {
+namespace testing {
+
+using ::envoy::config::cluster::v3::Cluster;
+using ::envoy::config::core::v3::HealthStatus;
+using ::envoy::config::endpoint::v3::ClusterLoadAssignment;
+using ::envoy::config::listener::v3::Listener;
+using ::envoy::config::route::v3::RouteConfiguration;
+using ::envoy::extensions::filters::network::http_connection_manager::v3::
+    HttpConnectionManager;
+
+//
+// XdsBootstrapBuilder
+//
+
+std::string XdsBootstrapBuilder::Build() {
+  std::vector<std::string> fields;
+  fields.push_back(MakeXdsServersText(top_server_));
+  if (!client_default_listener_resource_name_template_.empty()) {
+    fields.push_back(
+        absl::StrCat("  \"client_default_listener_resource_name_template\": \"",
+                     client_default_listener_resource_name_template_, "\""));
+  }
+  fields.push_back(MakeNodeText());
+  if (!server_listener_resource_name_template_.empty()) {
+    fields.push_back(
+        absl::StrCat("  \"server_listener_resource_name_template\": \"",
+                     server_listener_resource_name_template_, "\""));
+  }
+  fields.push_back(MakeCertificateProviderText());
+  fields.push_back(MakeAuthorityText());
+  return absl::StrCat("{", absl::StrJoin(fields, ",\n"), "}");
+}
+
+std::string XdsBootstrapBuilder::MakeXdsServersText(
+    absl::string_view server_uri) {
+  constexpr char kXdsServerTemplate[] =
+      "      \"xds_servers\": [\n"
+      "        {\n"
+      "          \"server_uri\": \"<SERVER_URI>\",\n"
+      "          \"channel_creds\": [\n"
+      "            {\n"
+      "              \"type\": \"fake\"\n"
+      "            }\n"
+      "          ],\n"
+      "          \"server_features\": [<SERVER_FEATURES>]\n"
+      "        }\n"
+      "      ]";
+  std::vector<std::string> server_features;
+  if (ignore_resource_deletion_) {
+    server_features.push_back("\"ignore_resource_deletion\"");
+  }
+  return absl::StrReplaceAll(
+      kXdsServerTemplate,
+      {{"<SERVER_URI>", server_uri},
+       {"<SERVER_FEATURES>", absl::StrJoin(server_features, ", ")}});
+}
+
+std::string XdsBootstrapBuilder::MakeNodeText() {
+  constexpr char kXdsNode[] =
+      "  \"node\": {\n"
+      "    \"id\": \"xds_end2end_test\",\n"
+      "    \"cluster\": \"test\",\n"
+      "    \"metadata\": {\n"
+      "      \"foo\": \"bar\"\n"
+      "    },\n"
+      "    \"locality\": {\n"
+      "      \"region\": \"corp\",\n"
+      "      \"zone\": \"svl\",\n"
+      "      \"sub_zone\": \"mp3\"\n"
+      "    }\n"
+      "  }";
+  return kXdsNode;
+}
+
+std::string XdsBootstrapBuilder::MakeCertificateProviderText() {
+  std::vector<std::string> entries;
+  for (const auto& p : plugins_) {
+    const std::string& key = p.first;
+    const PluginInfo& plugin_info = p.second;
+    std::vector<std::string> fields;
+    fields.push_back(absl::StrFormat("    \"%s\": {", key));
+    if (!plugin_info.plugin_config.empty()) {
+      fields.push_back(
+          absl::StrFormat("      \"plugin_name\": \"%s\",", plugin_info.name));
+      fields.push_back(absl::StrCat("      \"config\": {\n",
+                                    plugin_info.plugin_config, "\n      }"));
+    } else {
+      fields.push_back(
+          absl::StrFormat("      \"plugin_name\": \"%s\"", plugin_info.name));
+    }
+    fields.push_back("    }");
+    entries.push_back(absl::StrJoin(fields, "\n"));
+  }
+  return absl::StrCat("  \"certificate_providers\": {\n",
+                      absl::StrJoin(entries, ",\n"), "  \n}");
+}
+
+std::string XdsBootstrapBuilder::MakeAuthorityText() {
+  std::vector<std::string> entries;
+  for (const auto& p : authorities_) {
+    const std::string& name = p.first;
+    const AuthorityInfo& authority_info = p.second;
+    std::vector<std::string> fields = {
+        MakeXdsServersText(authority_info.server)};
+    if (!authority_info.client_listener_resource_name_template.empty()) {
+      fields.push_back(absl::StrCat(
+          "\"client_listener_resource_name_template\": \"",
+          authority_info.client_listener_resource_name_template, "\""));
+    }
+    entries.push_back(absl::StrCat(absl::StrFormat("\"%s\": {\n  ", name),
+                                   absl::StrJoin(fields, ",\n"), "\n}"));
+  }
+  return absl::StrCat("\"authorities\": {\n", absl::StrJoin(entries, ",\n"),
+                      "\n}");
+}
+
+//
+// XdsResourceUtils::ClientHcmAccessor
+//
+
+HttpConnectionManager XdsResourceUtils::ClientHcmAccessor::Unpack(
+    const Listener& listener) const {
+  HttpConnectionManager http_connection_manager;
+  listener.api_listener().api_listener().UnpackTo(&http_connection_manager);
+  return http_connection_manager;
+}
+
+void XdsResourceUtils::ClientHcmAccessor::Pack(const HttpConnectionManager& hcm,
+                                               Listener* listener) const {
+  auto* api_listener = listener->mutable_api_listener()->mutable_api_listener();
+  api_listener->PackFrom(hcm);
+}
+
+//
+// XdsResourceUtils::ServerHcmAccessor
+//
+
+HttpConnectionManager XdsResourceUtils::ServerHcmAccessor::Unpack(
+    const Listener& listener) const {
+  HttpConnectionManager http_connection_manager;
+  listener.default_filter_chain().filters().at(0).typed_config().UnpackTo(
+      &http_connection_manager);
+  return http_connection_manager;
+}
+
+void XdsResourceUtils::ServerHcmAccessor::Pack(const HttpConnectionManager& hcm,
+                                               Listener* listener) const {
+  auto* filters = listener->mutable_default_filter_chain()->mutable_filters();
+  if (filters->empty()) filters->Add();
+  filters->at(0).mutable_typed_config()->PackFrom(hcm);
+}
+
+//
+// XdsResourceUtils
+//
+
+const char XdsResourceUtils::kDefaultLocalityRegion[] =
+    "xds_default_locality_region";
+const char XdsResourceUtils::kDefaultLocalityZone[] =
+    "xds_default_locality_zone";
+
+const char XdsResourceUtils::kServerName[] = "server.example.com";
+const char XdsResourceUtils::kDefaultRouteConfigurationName[] =
+    "route_config_name";
+const char XdsResourceUtils::kDefaultClusterName[] = "cluster_name";
+const char XdsResourceUtils::kDefaultEdsServiceName[] = "eds_service_name";
+const char XdsResourceUtils::kDefaultServerRouteConfigurationName[] =
+    "default_server_route_config_name";
+
+Listener XdsResourceUtils::DefaultListener() {
+  Listener listener;
+  listener.set_name(kServerName);
+  ClientHcmAccessor().Pack(DefaultHcm(), &listener);
+  return listener;
+}
+
+RouteConfiguration XdsResourceUtils::DefaultRouteConfig() {
+  RouteConfiguration route_config;
+  route_config.set_name(kDefaultRouteConfigurationName);
+  auto* virtual_host = route_config.add_virtual_hosts();
+  virtual_host->add_domains("*");
+  auto* route = virtual_host->add_routes();
+  route->mutable_match()->set_prefix("");
+  route->mutable_route()->set_cluster(kDefaultClusterName);
+  return route_config;
+}
+
+Cluster XdsResourceUtils::DefaultCluster() {
+  Cluster cluster;
+  cluster.set_name(kDefaultClusterName);
+  cluster.set_type(Cluster::EDS);
+  auto* eds_config = cluster.mutable_eds_cluster_config();
+  eds_config->mutable_eds_config()->mutable_self();
+  eds_config->set_service_name(kDefaultEdsServiceName);
+  cluster.set_lb_policy(Cluster::ROUND_ROBIN);
+  return cluster;
+}
+
+Listener XdsResourceUtils::DefaultServerListener() {
+  Listener listener;
+  listener.mutable_address()->mutable_socket_address()->set_address(
+      grpc_core::LocalIp());
+  ServerHcmAccessor().Pack(DefaultHcm(), &listener);
+  return listener;
+}
+
+RouteConfiguration XdsResourceUtils::DefaultServerRouteConfig() {
+  RouteConfiguration route_config;
+  route_config.set_name(kDefaultServerRouteConfigurationName);
+  auto* virtual_host = route_config.add_virtual_hosts();
+  virtual_host->add_domains("*");
+  auto* route = virtual_host->add_routes();
+  route->mutable_match()->set_prefix("");
+  route->mutable_non_forwarding_action();
+  return route_config;
+}
+
+HttpConnectionManager XdsResourceUtils::DefaultHcm() {
+  HttpConnectionManager http_connection_manager;
+  auto* filter = http_connection_manager.add_http_filters();
+  filter->set_name("router");
+  filter->mutable_typed_config()->PackFrom(
+      envoy::extensions::filters::http::router::v3::Router());
+  return http_connection_manager;
+}
+
+std::string XdsResourceUtils::GetServerListenerName(int port) {
+  return absl::StrCat("grpc/server?xds.resource.listening_address=",
+                      grpc_core::LocalIp(), ":", port);
+}
+
+Listener XdsResourceUtils::PopulateServerListenerNameAndPort(
+    const Listener& listener_template, int port) {
+  Listener listener = listener_template;
+  listener.set_name(GetServerListenerName(port));
+  listener.mutable_address()->mutable_socket_address()->set_port_value(port);
+  return listener;
+}
+
+void XdsResourceUtils::SetListenerAndRouteConfiguration(
+    AdsServiceImpl* ads_service, Listener listener,
+    const RouteConfiguration& route_config, bool use_rds,
+    const HcmAccessor& hcm_accessor) {
+  HttpConnectionManager http_connection_manager = hcm_accessor.Unpack(listener);
+  if (use_rds) {
+    auto* rds = http_connection_manager.mutable_rds();
+    rds->set_route_config_name(route_config.name());
+    rds->mutable_config_source()->mutable_self();
+    ads_service->SetRdsResource(route_config);
+  } else {
+    *http_connection_manager.mutable_route_config() = route_config;
+  }
+  hcm_accessor.Pack(http_connection_manager, &listener);
+  ads_service->SetLdsResource(listener);
+}
+
+void XdsResourceUtils::SetRouteConfiguration(
+    AdsServiceImpl* ads_service, const RouteConfiguration& route_config,
+    bool use_rds, const Listener* listener_to_copy) {
+  if (use_rds) {
+    ads_service->SetRdsResource(route_config);
+  } else {
+    Listener listener(listener_to_copy == nullptr ? DefaultListener()
+                                                  : *listener_to_copy);
+    HttpConnectionManager http_connection_manager =
+        ClientHcmAccessor().Unpack(listener);
+    *(http_connection_manager.mutable_route_config()) = route_config;
+    ClientHcmAccessor().Pack(http_connection_manager, &listener);
+    ads_service->SetLdsResource(listener);
+  }
+}
+
+ClusterLoadAssignment XdsResourceUtils::BuildEdsResource(
+    const EdsResourceArgs& args, absl::string_view eds_service_name) {
+  ClusterLoadAssignment assignment;
+  assignment.set_cluster_name(eds_service_name);
+  for (const auto& locality : args.locality_list) {
+    auto* endpoints = assignment.add_endpoints();
+    endpoints->mutable_load_balancing_weight()->set_value(locality.lb_weight);
+    endpoints->set_priority(locality.priority);
+    endpoints->mutable_locality()->set_region(kDefaultLocalityRegion);
+    endpoints->mutable_locality()->set_zone(kDefaultLocalityZone);
+    endpoints->mutable_locality()->set_sub_zone(locality.sub_zone);
+    for (size_t i = 0; i < locality.endpoints.size(); ++i) {
+      const auto& endpoint = locality.endpoints[i];
+      auto* lb_endpoints = endpoints->add_lb_endpoints();
+      if (locality.endpoints.size() > i &&
+          locality.endpoints[i].health_status != HealthStatus::UNKNOWN) {
+        lb_endpoints->set_health_status(endpoint.health_status);
+      }
+      if (locality.endpoints.size() > i && endpoint.lb_weight >= 1) {
+        lb_endpoints->mutable_load_balancing_weight()->set_value(
+            endpoint.lb_weight);
+      }
+      auto* endpoint_proto = lb_endpoints->mutable_endpoint();
+      auto* socket_address =
+          endpoint_proto->mutable_address()->mutable_socket_address();
+      socket_address->set_address(grpc_core::LocalIp());
+      socket_address->set_port_value(endpoint.port);
+      for (int port : endpoint.additional_ports) {
+        socket_address = endpoint_proto->add_additional_addresses()
+                             ->mutable_address()
+                             ->mutable_socket_address();
+        socket_address->set_address(grpc_core::LocalIp());
+        socket_address->set_port_value(port);
+      }
+    }
+  }
+  if (!args.drop_categories.empty()) {
+    auto* policy = assignment.mutable_policy();
+    for (const auto& p : args.drop_categories) {
+      const std::string& name = p.first;
+      const uint32_t parts_per_million = p.second;
+      auto* drop_overload = policy->add_drop_overloads();
+      drop_overload->set_category(name);
+      auto* drop_percentage = drop_overload->mutable_drop_percentage();
+      drop_percentage->set_numerator(parts_per_million);
+      drop_percentage->set_denominator(args.drop_denominator);
+    }
+  }
+  return assignment;
+}
+
+}  // namespace testing
+}  // namespace grpc
diff --git a/test/cpp/end2end/xds/xds_utils.h b/test/cpp/end2end/xds/xds_utils.h
new file mode 100644
index 00000000000..5797d9c4835
--- /dev/null
+++ b/test/cpp/end2end/xds/xds_utils.h
@@ -0,0 +1,248 @@
+// Copyright 2017 gRPC authors.
+//
+// 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
+//
+//     http://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.
+//
+
+#ifndef GRPC_TEST_CPP_END2END_XDS_XDS_UTILS_H
+#define GRPC_TEST_CPP_END2END_XDS_XDS_UTILS_H
+
+#include <string>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+
+#include "src/proto/grpc/testing/xds/v3/cluster.pb.h"
+#include "src/proto/grpc/testing/xds/v3/endpoint.pb.h"
+#include "src/proto/grpc/testing/xds/v3/http_connection_manager.pb.h"
+#include "src/proto/grpc/testing/xds/v3/listener.pb.h"
+#include "src/proto/grpc/testing/xds/v3/route.pb.h"
+#include "test/cpp/end2end/xds/xds_server.h"
+
+namespace grpc {
+namespace testing {
+
+// A builder for the xDS bootstrap config.
+class XdsBootstrapBuilder {
+ public:
+  XdsBootstrapBuilder() {}
+  XdsBootstrapBuilder& SetIgnoreResourceDeletion() {
+    ignore_resource_deletion_ = true;
+    return *this;
+  }
+  // If ignore_if_set is true, sets the default server only if it has
+  // not already been set.
+  XdsBootstrapBuilder& SetDefaultServer(const std::string& server,
+                                        bool ignore_if_set = false) {
+    if (!ignore_if_set || top_server_.empty()) top_server_ = server;
+    return *this;
+  }
+  XdsBootstrapBuilder& SetClientDefaultListenerResourceNameTemplate(
+      const std::string& client_default_listener_resource_name_template) {
+    client_default_listener_resource_name_template_ =
+        client_default_listener_resource_name_template;
+    return *this;
+  }
+  XdsBootstrapBuilder& AddCertificateProviderPlugin(
+      const std::string& key, const std::string& name,
+      const std::string& plugin_config = "") {
+    plugins_[key] = {name, plugin_config};
+    return *this;
+  }
+  XdsBootstrapBuilder& AddAuthority(
+      const std::string& authority, const std::string& server = "",
+      const std::string& client_listener_resource_name_template = "") {
+    authorities_[authority] = {server, client_listener_resource_name_template};
+    return *this;
+  }
+  XdsBootstrapBuilder& SetServerListenerResourceNameTemplate(
+      const std::string& server_listener_resource_name_template = "") {
+    server_listener_resource_name_template_ =
+        server_listener_resource_name_template;
+    return *this;
+  }
+
+  std::string Build();
+
+ private:
+  struct PluginInfo {
+    std::string name;
+    std::string plugin_config;
+  };
+  struct AuthorityInfo {
+    std::string server;
+    std::string client_listener_resource_name_template;
+  };
+
+  std::string MakeXdsServersText(absl::string_view server_uri);
+  std::string MakeNodeText();
+  std::string MakeCertificateProviderText();
+  std::string MakeAuthorityText();
+
+  bool ignore_resource_deletion_ = false;
+  std::string top_server_;
+  std::string client_default_listener_resource_name_template_;
+  std::map<std::string /*key*/, PluginInfo> plugins_;
+  std::map<std::string /*authority_name*/, AuthorityInfo> authorities_;
+  std::string server_listener_resource_name_template_ =
+      "grpc/server?xds.resource.listening_address=%s";
+};
+
+// Utilities for constructing xDS resources.
+class XdsResourceUtils {
+ public:
+  using HttpConnectionManager = envoy::extensions::filters::network::
+      http_connection_manager::v3::HttpConnectionManager;
+  using Listener = envoy::config::listener::v3::Listener;
+  using RouteConfiguration = envoy::config::route::v3::RouteConfiguration;
+  using Cluster = envoy::config::cluster::v3::Cluster;
+  using ClusterLoadAssignment =
+      envoy::config::endpoint::v3::ClusterLoadAssignment;
+
+  // Interface for accessing HttpConnectionManager config in Listener.
+  class HcmAccessor {
+   public:
+    virtual ~HcmAccessor() = default;
+    virtual HttpConnectionManager Unpack(const Listener& listener) const = 0;
+    virtual void Pack(const HttpConnectionManager& hcm,
+                      Listener* listener) const = 0;
+  };
+
+  // Client-side impl.
+  class ClientHcmAccessor : public HcmAccessor {
+   public:
+    HttpConnectionManager Unpack(const Listener& listener) const override;
+    void Pack(const HttpConnectionManager& hcm,
+              Listener* listener) const override;
+  };
+
+  // Server-side impl.
+  class ServerHcmAccessor : public HcmAccessor {
+   public:
+    HttpConnectionManager Unpack(const Listener& listener) const override;
+    void Pack(const HttpConnectionManager& hcm,
+              Listener* listener) const override;
+  };
+
+  // Default values for locality fields.
+  static const char kDefaultLocalityRegion[];
+  static const char kDefaultLocalityZone[];
+  static const uint32_t kDefaultLocalityWeight = 3;
+  static const int kDefaultLocalityPriority = 0;
+
+  // Default resource names.
+  static const char kServerName[];
+  static const char kDefaultRouteConfigurationName[];
+  static const char kDefaultClusterName[];
+  static const char kDefaultEdsServiceName[];
+  static const char kDefaultServerRouteConfigurationName[];
+
+  // Returns default xDS resources.
+  static Listener DefaultListener();
+  static RouteConfiguration DefaultRouteConfig();
+  static Cluster DefaultCluster();
+  static Listener DefaultServerListener();
+  static RouteConfiguration DefaultServerRouteConfig();
+  static HttpConnectionManager DefaultHcm();
+
+  // Returns the name of the server-side xDS Listener resource for a
+  // backend on the specified port.
+  static std::string GetServerListenerName(int port);
+
+  // Returns a copy of listener_template with the server-side resource
+  // name and the port in the socket address populated.
+  static Listener PopulateServerListenerNameAndPort(
+      const Listener& listener_template, int port);
+
+  // Sets the Listener and RouteConfiguration resource on the specified
+  // balancer.  If RDS is in use, they will be set as separate resources;
+  // otherwise, the RouteConfig will be inlined into the Listener.
+  static void SetListenerAndRouteConfiguration(
+      AdsServiceImpl* ads_service, Listener listener,
+      const RouteConfiguration& route_config, bool use_rds = false,
+      const HcmAccessor& hcm_accessor = ClientHcmAccessor());
+
+  // A convenient wrapper for setting the Listener and
+  // RouteConfiguration resources on the server side.
+  static void SetServerListenerNameAndRouteConfiguration(
+      AdsServiceImpl* ads_service, Listener listener, int port,
+      const RouteConfiguration& route_config, bool use_rds = false) {
+    SetListenerAndRouteConfiguration(
+        ads_service, PopulateServerListenerNameAndPort(listener, port),
+        route_config, use_rds, ServerHcmAccessor());
+  }
+
+  // Sets the RouteConfiguration resource on the specified balancer.
+  // If RDS is in use, it will be set directly as an independent
+  // resource; otherwise, it will be inlined into a Listener resource
+  // (either listener_to_copy, or if that is null, default_listener_).
+  static void SetRouteConfiguration(AdsServiceImpl* ads_service,
+                                    const RouteConfiguration& route_config,
+                                    bool use_rds = false,
+                                    const Listener* listener_to_copy = nullptr);
+
+  // Arguments for constructing an EDS resource.
+  struct EdsResourceArgs {
+    // An individual endpoint for a backend running on a specified port.
+    struct Endpoint {
+      explicit Endpoint(int port,
+                        ::envoy::config::core::v3::HealthStatus health_status =
+                            ::envoy::config::core::v3::HealthStatus::UNKNOWN,
+                        int lb_weight = 1,
+                        std::vector<int> additional_ports = {})
+          : port(port),
+            health_status(health_status),
+            lb_weight(lb_weight),
+            additional_ports(std::move(additional_ports)) {}
+
+      int port;
+      ::envoy::config::core::v3::HealthStatus health_status;
+      int lb_weight;
+      std::vector<int> additional_ports;
+    };
+
+    // A locality.
+    struct Locality {
+      Locality(std::string sub_zone, std::vector<Endpoint> endpoints,
+               uint32_t lb_weight = kDefaultLocalityWeight,
+               int priority = kDefaultLocalityPriority)
+          : sub_zone(std::move(sub_zone)),
+            endpoints(std::move(endpoints)),
+            lb_weight(lb_weight),
+            priority(priority) {}
+
+      const std::string sub_zone;
+      std::vector<Endpoint> endpoints;
+      uint32_t lb_weight;
+      int priority;
+    };
+
+    EdsResourceArgs() = default;
+    explicit EdsResourceArgs(std::vector<Locality> locality_list)
+        : locality_list(std::move(locality_list)) {}
+
+    std::vector<Locality> locality_list;
+    std::map<std::string, uint32_t> drop_categories;
+    ::envoy::type::v3::FractionalPercent::DenominatorType drop_denominator =
+        ::envoy::type::v3::FractionalPercent::MILLION;
+  };
+
+  // Constructs an EDS resource.
+  static ClusterLoadAssignment BuildEdsResource(
+      const EdsResourceArgs& args,
+      absl::string_view eds_service_name = kDefaultEdsServiceName);
+};
+
+}  // namespace testing
+}  // namespace grpc
+
+#endif  // GRPC_TEST_CPP_END2END_XDS_XDS_UTILS_H