diff --git a/build/charts/antrea/crds/packetcapture.yaml b/build/charts/antrea/crds/packetcapture.yaml index 013a878b241..62aea5bbaea 100644 --- a/build/charts/antrea/crds/packetcapture.yaml +++ b/build/charts/antrea/crds/packetcapture.yaml @@ -161,6 +161,9 @@ spec: url: type: string pattern: 'sftp:\/\/[\w-_./]+:\d+' + hostPublicKey: + type: string + format: byte status: type: object properties: diff --git a/build/charts/antrea/crds/supportbundlecollection.yaml b/build/charts/antrea/crds/supportbundlecollection.yaml index 0487cfd4925..62795a5ec6c 100644 --- a/build/charts/antrea/crds/supportbundlecollection.yaml +++ b/build/charts/antrea/crds/supportbundlecollection.yaml @@ -100,6 +100,9 @@ spec: properties: url: type: string + hostPublicKey: + type: string + format: byte authentication: type: object properties: diff --git a/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml b/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml index 7873a036bb9..2f38d1a5eee 100644 --- a/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml +++ b/build/charts/antrea/templates/webhooks/validating/crdvalidator.yaml @@ -161,7 +161,7 @@ webhooks: namespace: {{ .Release.Namespace }} path: "/validate/supportbundlecollection" rules: - - operations: ["UPDATE", "DELETE"] + - operations: ["CREATE", "UPDATE", "DELETE"] apiGroups: ["crd.antrea.io"] apiVersions: ["v1alpha1"] resources: ["supportbundlecollections"] diff --git a/build/yamls/antrea-aks.yml b/build/yamls/antrea-aks.yml index 9a3af9a2496..c7c7798c1dc 100644 --- a/build/yamls/antrea-aks.yml +++ b/build/yamls/antrea-aks.yml @@ -3061,6 +3061,9 @@ spec: url: type: string pattern: 'sftp:\/\/[\w-_./]+:\d+' + hostPublicKey: + type: string + format: byte status: type: object properties: @@ -3197,6 +3200,9 @@ spec: properties: url: type: string + hostPublicKey: + type: string + format: byte authentication: type: object properties: @@ -5999,7 +6005,7 @@ webhooks: namespace: kube-system path: "/validate/supportbundlecollection" rules: - - operations: ["UPDATE", "DELETE"] + - operations: ["CREATE", "UPDATE", "DELETE"] apiGroups: ["crd.antrea.io"] apiVersions: ["v1alpha1"] resources: ["supportbundlecollections"] diff --git a/build/yamls/antrea-crds.yml b/build/yamls/antrea-crds.yml index 436f15f0ddf..9300377ee62 100644 --- a/build/yamls/antrea-crds.yml +++ b/build/yamls/antrea-crds.yml @@ -3034,6 +3034,9 @@ spec: url: type: string pattern: 'sftp:\/\/[\w-_./]+:\d+' + hostPublicKey: + type: string + format: byte status: type: object properties: @@ -3168,6 +3171,9 @@ spec: properties: url: type: string + hostPublicKey: + type: string + format: byte authentication: type: object properties: diff --git a/build/yamls/antrea-eks.yml b/build/yamls/antrea-eks.yml index 57a3a73f63e..c2428bf8d85 100644 --- a/build/yamls/antrea-eks.yml +++ b/build/yamls/antrea-eks.yml @@ -3061,6 +3061,9 @@ spec: url: type: string pattern: 'sftp:\/\/[\w-_./]+:\d+' + hostPublicKey: + type: string + format: byte status: type: object properties: @@ -3197,6 +3200,9 @@ spec: properties: url: type: string + hostPublicKey: + type: string + format: byte authentication: type: object properties: @@ -6000,7 +6006,7 @@ webhooks: namespace: kube-system path: "/validate/supportbundlecollection" rules: - - operations: ["UPDATE", "DELETE"] + - operations: ["CREATE", "UPDATE", "DELETE"] apiGroups: ["crd.antrea.io"] apiVersions: ["v1alpha1"] resources: ["supportbundlecollections"] diff --git a/build/yamls/antrea-gke.yml b/build/yamls/antrea-gke.yml index 707ffd8294c..e1f07fc564c 100644 --- a/build/yamls/antrea-gke.yml +++ b/build/yamls/antrea-gke.yml @@ -3061,6 +3061,9 @@ spec: url: type: string pattern: 'sftp:\/\/[\w-_./]+:\d+' + hostPublicKey: + type: string + format: byte status: type: object properties: @@ -3197,6 +3200,9 @@ spec: properties: url: type: string + hostPublicKey: + type: string + format: byte authentication: type: object properties: @@ -5997,7 +6003,7 @@ webhooks: namespace: kube-system path: "/validate/supportbundlecollection" rules: - - operations: ["UPDATE", "DELETE"] + - operations: ["CREATE", "UPDATE", "DELETE"] apiGroups: ["crd.antrea.io"] apiVersions: ["v1alpha1"] resources: ["supportbundlecollections"] diff --git a/build/yamls/antrea-ipsec.yml b/build/yamls/antrea-ipsec.yml index 2f98a17d5f8..dbb2119becb 100644 --- a/build/yamls/antrea-ipsec.yml +++ b/build/yamls/antrea-ipsec.yml @@ -3061,6 +3061,9 @@ spec: url: type: string pattern: 'sftp:\/\/[\w-_./]+:\d+' + hostPublicKey: + type: string + format: byte status: type: object properties: @@ -3197,6 +3200,9 @@ spec: properties: url: type: string + hostPublicKey: + type: string + format: byte authentication: type: object properties: @@ -6056,7 +6062,7 @@ webhooks: namespace: kube-system path: "/validate/supportbundlecollection" rules: - - operations: ["UPDATE", "DELETE"] + - operations: ["CREATE", "UPDATE", "DELETE"] apiGroups: ["crd.antrea.io"] apiVersions: ["v1alpha1"] resources: ["supportbundlecollections"] diff --git a/build/yamls/antrea.yml b/build/yamls/antrea.yml index 0bd56ca6cd1..0580bbfb76b 100644 --- a/build/yamls/antrea.yml +++ b/build/yamls/antrea.yml @@ -3061,6 +3061,9 @@ spec: url: type: string pattern: 'sftp:\/\/[\w-_./]+:\d+' + hostPublicKey: + type: string + format: byte status: type: object properties: @@ -3197,6 +3200,9 @@ spec: properties: url: type: string + hostPublicKey: + type: string + format: byte authentication: type: object properties: @@ -5997,7 +6003,7 @@ webhooks: namespace: kube-system path: "/validate/supportbundlecollection" rules: - - operations: ["UPDATE", "DELETE"] + - operations: ["CREATE", "UPDATE", "DELETE"] apiGroups: ["crd.antrea.io"] apiVersions: ["v1alpha1"] resources: ["supportbundlecollections"] diff --git a/docs/packetcapture-guide.md b/docs/packetcapture-guide.md index 044f791ff1e..d46379c40cc 100644 --- a/docs/packetcapture-guide.md +++ b/docs/packetcapture-guide.md @@ -57,6 +57,10 @@ metadata: spec: fileServer: url: sftp://127.0.0.1:22/upload # Define your own sftp url here. + # Host public key (as a base64-encoded string) that will be accepted when connecting to the file server. + # Get this key from your SSH server configuration, or from a known_hosts file. + # If omitted, any host key will be accepted, which is insecure and not recommended. + hostPublicKey: AAAAC3NzaC1lZDI1NTE5AAAAIBCUI6Yi9KbkiPXK2MzqYYtlluw7v_WQz071JZPdZEKr # Replace with your own. timeout: 60 captureConfig: firstN: diff --git a/docs/support-bundle-guide.md b/docs/support-bundle-guide.md index 88930e8fea9..2037db709ce 100644 --- a/docs/support-bundle-guide.md +++ b/docs/support-bundle-guide.md @@ -135,6 +135,10 @@ spec: sinceTime: 2h # Collect the logs in the latest 2 hours. Collect all available logs if the time is not set. fileServer: url: sftp://yourtestdomain.com:22/root/test + # Host public key (as a base64-encoded string) that will be accepted when connecting to the file server. + # Get this key from your SSH server configuration, or from a known_hosts file. + # If omitted, any host key will be accepted, which is insecure and not recommended. + # hostPublicKey: ... authentication: authType: "BasicAuthentication" authSecret: @@ -159,6 +163,10 @@ spec: namespace: vm-ns # namespace is mandatory when collecting support bundle from external Nodes. fileServer: url: yourtestdomain.com:22/root/test # Scheme sftp can be omitted. The url of "$controlplane_node_ip:30010/upload" is used if deployed with sftp-deployment.yml. + # Host public key (as a base64-encoded string) that will be accepted when connecting to the file server. + # Get this key from your SSH server configuration, or from a known_hosts file. + # If omitted, any host key will be accepted, which is insecure and not recommended. + # hostPublicKey: ... authentication: authType: "BasicAuthentication" authSecret: diff --git a/pkg/agent/packetcapture/packetcapture_controller.go b/pkg/agent/packetcapture/packetcapture_controller.go index 5282ee904af..c799ffd2179 100644 --- a/pkg/agent/packetcapture/packetcapture_controller.go +++ b/pkg/agent/packetcapture/packetcapture_controller.go @@ -31,7 +31,6 @@ import ( "github.com/gopacket/gopacket/layers" "github.com/gopacket/gopacket/pcapgo" "github.com/spf13/afero" - "golang.org/x/crypto/ssh" "golang.org/x/time/rate" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -585,12 +584,13 @@ func (c *Controller) uploadPackets(ctx context.Context, pc *crdv1alpha1.PacketCa if serverAuth.BasicAuthentication == nil { return fmt.Errorf("failed to get basic authentication info for the file server") } - cfg := &ssh.ClientConfig{ - User: serverAuth.BasicAuthentication.Username, - Auth: []ssh.AuthMethod{ssh.Password(serverAuth.BasicAuthentication.Password)}, - // #nosec G106: skip host key check here and users can specify their own checks if needed - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: time.Second, + cfg, err := sftp.GetSSHClientConfig( + serverAuth.BasicAuthentication.Username, + serverAuth.BasicAuthentication.Password, + pc.Spec.FileServer.HostPublicKey, + ) + if err != nil { + return fmt.Errorf("failed to generate SSH client config: %w", err) } return uploader.Upload(pc.Spec.FileServer.URL, c.generatePacketsPathForServer(pc.Name), cfg, outputFile) } diff --git a/pkg/agent/packetcapture/packetcapture_controller_test.go b/pkg/agent/packetcapture/packetcapture_controller_test.go index 29f0f6fe62b..903c7815482 100644 --- a/pkg/agent/packetcapture/packetcapture_controller_test.go +++ b/pkg/agent/packetcapture/packetcapture_controller_test.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "net" + "slices" "testing" "time" @@ -46,6 +47,7 @@ import ( fakeversioned "antrea.io/antrea/pkg/client/clientset/versioned/fake" crdinformers "antrea.io/antrea/pkg/client/informers/externalversions" "antrea.io/antrea/pkg/util/k8s" + sftptesting "antrea.io/antrea/pkg/util/sftp/testing" ) var ( @@ -145,17 +147,22 @@ func genTestCR(name string, num int32) *crdv1alpha1.PacketCapture { type testUploader struct { url string fileName string - // for concurrent cases, no need to check - checkFileName bool + hostKey ssh.PublicKey } func (uploader *testUploader) Upload(url string, fileName string, config *ssh.ClientConfig, outputFile io.Reader) error { if url != uploader.url { return fmt.Errorf("expected url: %s for uploader, got: %s", uploader.url, url) } - if uploader.checkFileName { - if fileName != uploader.fileName { - return fmt.Errorf("expected filename: %s, got: %s ", uploader.fileName, fileName) + if uploader.fileName != "" && fileName != uploader.fileName { + return fmt.Errorf("expected filename: %s, got: %s ", uploader.fileName, fileName) + } + if uploader.hostKey != nil { + if config.HostKeyAlgorithms != nil && !slices.Equal(config.HostKeyAlgorithms, []string{uploader.hostKey.Type()}) { + return fmt.Errorf("unsupported host key algorithm") + } + if err := config.HostKeyCallback("", nil, uploader.hostKey); err != nil { + return fmt.Errorf("invalid host key: %w", err) } } return nil @@ -594,3 +601,69 @@ func TestMergeConditions(t *testing.T) { }) } } + +func TestUploadPackets(t *testing.T) { + ctx := context.Background() + + generateHostKey := func(t *testing.T) ssh.PublicKey { + publicKey, _, err := sftptesting.GenerateEd25519Key() + require.NoError(t, err) + return publicKey + } + hostKey1 := generateHostKey(t) + hostKey2 := generateHostKey(t) + + fs := afero.NewMemMapFs() + + testCases := []struct { + name string + serverHostKey ssh.PublicKey + expectedHostKey []byte + expectedErr string + }{ + { + name: "matching key", + serverHostKey: hostKey1, + expectedHostKey: hostKey1.Marshal(), + }, + { + name: "non matching key", + serverHostKey: hostKey2, + expectedHostKey: hostKey1.Marshal(), + expectedErr: "host key mismatch", + }, + { + name: "ignore host key", + serverHostKey: hostKey1, + expectedHostKey: nil, + }, + { + name: "invalid key format", + serverHostKey: hostKey1, + expectedHostKey: []byte("abc"), + expectedErr: "invalid host public key", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pc := genTestCR("foo", testCaptureNum) + pcc := newFakePacketCaptureController(t, nil, nil) + pcc.sftpUploader = &testUploader{ + url: testFTPUrl, + fileName: pcc.generatePacketsPathForServer(pc.Name), + hostKey: tc.serverHostKey, + } + pc.Spec.FileServer.HostPublicKey = tc.expectedHostKey + f, err := afero.TempFile(fs, "", "upload-test") + require.NoError(t, err) + defer f.Close() + err = pcc.uploadPackets(ctx, pc, f) + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, tc.expectedErr) + } + }) + } +} diff --git a/pkg/agent/supportbundlecollection/support_bundle_controller.go b/pkg/agent/supportbundlecollection/support_bundle_controller.go index 9d12536d377..5aae1bb1196 100644 --- a/pkg/agent/supportbundlecollection/support_bundle_controller.go +++ b/pkg/agent/supportbundlecollection/support_bundle_controller.go @@ -22,7 +22,6 @@ import ( "time" "github.com/spf13/afero" - "golang.org/x/crypto/ssh" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/util/wait" @@ -300,14 +299,14 @@ func (c *SupportBundleController) uploadSupportBundle(supportBundle *cpv1b2.Supp return fmt.Errorf("failed to upload to the file server while setting offset: %v", err) } fileName := c.nodeName + "_" + supportBundle.Name + ".tar.gz" - cfg := &ssh.ClientConfig{ - User: supportBundle.Authentication.BasicAuthentication.Username, - Auth: []ssh.AuthMethod{ssh.Password(supportBundle.Authentication.BasicAuthentication.Password)}, - // #nosec G106: skip host key check here and users can specify their own checks if needed - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: time.Second, + cfg, err := sftp.GetSSHClientConfig( + supportBundle.Authentication.BasicAuthentication.Username, + supportBundle.Authentication.BasicAuthentication.Password, + supportBundle.FileServer.HostPublicKey, + ) + if err != nil { + return fmt.Errorf("failed to generate SSH client config: %w", err) } - return uploader.Upload(supportBundle.FileServer.URL, fileName, cfg, outputFile) } diff --git a/pkg/agent/supportbundlecollection/support_bundle_controller_test.go b/pkg/agent/supportbundlecollection/support_bundle_controller_test.go index 5589d189fbc..5ec42ac2788 100644 --- a/pkg/agent/supportbundlecollection/support_bundle_controller_test.go +++ b/pkg/agent/supportbundlecollection/support_bundle_controller_test.go @@ -17,10 +17,12 @@ package supportbundlecollection import ( "fmt" "io" + "slices" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "golang.org/x/crypto/ssh" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -38,6 +40,7 @@ import ( "antrea.io/antrea/pkg/querier" "antrea.io/antrea/pkg/support" "antrea.io/antrea/pkg/util/sftp" + sftptesting "antrea.io/antrea/pkg/util/sftp/testing" ) type fakeController struct { @@ -65,103 +68,140 @@ func newFakeController(t *testing.T) (*fakeController, *fakeversioned.Clientset) } func TestSupportBundleCollectionAdd(t *testing.T) { + uploadErr := fmt.Errorf("upload failed") + generateHostKey := func(t *testing.T) ssh.PublicKey { + publicKey, _, err := sftptesting.GenerateEd25519Key() + require.NoError(t, err) + return publicKey + } + hostKey1 := generateHostKey(t) + hostKey2 := generateHostKey(t) + testcases := []struct { name string supportBundleCollection *cpv1b2.SupportBundleCollection - expectedCompleted bool agentDumper *mockAgentDumper uploader sftp.Uploader + expectedSyncErr string }{ { name: "Add SupportBundleCollection", - supportBundleCollection: generateSupportbundleCollection("supportBundle1", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: true, + supportBundleCollection: generateSupportbundleCollection("supportBundle1", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{}, uploader: &testUploader{}, }, { name: "Add SupportBundleCollection without url prefix", - supportBundleCollection: generateSupportbundleCollection("supportBundle2", "10.220.175.92:22/root/supportbundle"), - expectedCompleted: true, + supportBundleCollection: generateSupportbundleCollection("supportBundle2", "10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{}, uploader: &testUploader{}, }, { name: "Add SupportBundleCollection with unsupported url prefix", - supportBundleCollection: generateSupportbundleCollection("supportBundle3", "https://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle3", "https://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{}, - uploader: &testFailedUploader{}, + uploader: &testUploader{ + err: uploadErr, + }, + expectedSyncErr: uploadErr.Error(), }, { name: "Add SupportBundleCollection with retry logics", - supportBundleCollection: generateSupportbundleCollection("supportBundle4", "10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle4", "10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{}, - uploader: &testFailedUploader{}, + uploader: &testUploader{ + err: uploadErr, + }, + expectedSyncErr: uploadErr.Error(), }, { name: "SupportBundleCollection failed to dump log", - supportBundleCollection: generateSupportbundleCollection("supportBundle5", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle5", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpLogErr: fmt.Errorf("failed to dump log")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump log", }, { name: "SupportBundleCollection failed to dump flows", - supportBundleCollection: generateSupportbundleCollection("supportBundle6", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle6", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpFlowsErr: fmt.Errorf("failed to dump flows")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump flows", }, { name: "SupportBundleCollection failed to dump host network info", - supportBundleCollection: generateSupportbundleCollection("supportBundle7", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle7", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpHostNetworkInfoErr: fmt.Errorf("failed to dump host network info")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump host network info", }, { name: "SupportBundleCollection failed to dump agent info", - supportBundleCollection: generateSupportbundleCollection("supportBundle8", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle8", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpAgentInfoErr: fmt.Errorf("failed to dump agent info")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump agent info", }, { name: "SupportBundleCollection failed to dump network policy resources", - supportBundleCollection: generateSupportbundleCollection("supportBundle9", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle9", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpNetworkPolicyResourcesErr: fmt.Errorf("failed to dump network policy resources")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump network policy resources", }, { name: "SupportBundleCollection failed to dump heap Pprof", - supportBundleCollection: generateSupportbundleCollection("supportBundle10", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle10", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpHeapPprofErr: fmt.Errorf("failed to dump heap Pprof")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump heap Pprof", }, { name: "SupportBundleCollection failed to dump OVS ports", - supportBundleCollection: generateSupportbundleCollection("supportBundle11", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle11", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpOVSPortsErr: fmt.Errorf("failed to dump OVS ports")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump OVS ports", }, { name: "SupportBundleCollection failed to dump goroutine Pprof", - supportBundleCollection: generateSupportbundleCollection("supportBundle12", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle12", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpGoroutinePprofErr: fmt.Errorf("failed to dump goroutine Pprof")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump goroutine Pprof", }, { name: "SupportBundleCollection failed to dump groups", - supportBundleCollection: generateSupportbundleCollection("supportBundle13", "sftp://10.220.175.92:22/root/supportbundle"), - expectedCompleted: false, + supportBundleCollection: generateSupportbundleCollection("supportBundle13", "sftp://10.220.175.92:22/root/supportbundle", nil), agentDumper: &mockAgentDumper{dumpGroupsErr: fmt.Errorf("failed to dump groups")}, uploader: &testUploader{}, + expectedSyncErr: "failed to generate support bundle: failed to dump groups", + }, + { + name: "Add SupportBundleCollection with host key", + supportBundleCollection: generateSupportbundleCollection("supportBundle13", "sftp://10.220.175.92:22/root/supportbundle", hostKey1.Marshal()), + agentDumper: &mockAgentDumper{}, + uploader: &testUploader{ + hostKey: hostKey1, + }, + }, + { + name: "Add SupportBundleCollection with host key mismatch", + supportBundleCollection: generateSupportbundleCollection("supportBundle14", "sftp://10.220.175.92:22/root/supportbundle", hostKey1.Marshal()), + agentDumper: &mockAgentDumper{}, + uploader: &testUploader{ + hostKey: hostKey2, + }, + expectedSyncErr: "failed to generate support bundle: invalid host key: ssh: host key mismatch", + }, + { + name: "Add SupportBundleCollection with invalid host key", + supportBundleCollection: generateSupportbundleCollection("supportBundle15", "sftp://10.220.175.92:22/root/supportbundle", []byte("abc")), + agentDumper: &mockAgentDumper{}, + uploader: &testUploader{ + hostKey: hostKey1, + }, + expectedSyncErr: "failed to generate support bundle: failed to generate SSH client config: invalid host public key", }, } @@ -182,47 +222,59 @@ func TestSupportBundleCollectionAdd(t *testing.T) { return false, bundleStatus, nil })) controller.addSupportBundleCollection(tt.supportBundleCollection) - controller.syncSupportBundleCollection(tt.supportBundleCollection.Name) - assert.Equal(t, tt.expectedCompleted, bundleStatus.Nodes[0].Completed) + err := controller.syncSupportBundleCollection(tt.supportBundleCollection.Name) + if tt.expectedSyncErr == "" { + assert.NoError(t, err) + assert.True(t, bundleStatus.Nodes[0].Completed) + } else { + assert.ErrorContains(t, err, tt.expectedSyncErr) + assert.False(t, bundleStatus.Nodes[0].Completed) + } }) } } func TestSupportBundleCollectionDelete(t *testing.T) { controller, _ := newFakeController(t) - deletedBundle := generateSupportbundleCollection("deletedBundle", "sftp://10.220.175.92/root/supportbundle") + deletedBundle := generateSupportbundleCollection("deletedBundle", "sftp://10.220.175.92/root/supportbundle", nil) controller.addSupportBundleCollection(deletedBundle) controller.deleteSupportBundleCollection(deletedBundle) assert.NoError(t, controller.syncSupportBundleCollection("deletedBundle")) } type testUploader struct { + err error + hostKey ssh.PublicKey } func (uploader *testUploader) Upload(address string, path string, config *ssh.ClientConfig, tarGzFile io.Reader) error { - klog.Info("Called test uploader") + klog.InfoS("Called test uploader", "err", uploader.err) + if uploader.err != nil { + return uploader.err + } if _, err := sftp.ParseSFTPUploadUrl(address); err != nil { return err } + if uploader.hostKey != nil { + if config.HostKeyAlgorithms != nil && !slices.Equal(config.HostKeyAlgorithms, []string{uploader.hostKey.Type()}) { + return fmt.Errorf("unsupported host key algorithm") + } + if err := config.HostKeyCallback("", nil, uploader.hostKey); err != nil { + return fmt.Errorf("invalid host key: %w", err) + } + } return nil } -type testFailedUploader struct { -} - -func (uploader *testFailedUploader) Upload(address string, path string, config *ssh.ClientConfig, tarGzFile io.Reader) error { - klog.Info("Called test uploader for failed case") - return fmt.Errorf("uploader failed") -} - -func generateSupportbundleCollection(name string, url string) *cpv1b2.SupportBundleCollection { +func generateSupportbundleCollection(name string, url string, hostPublicKey []byte) *cpv1b2.SupportBundleCollection { return &cpv1b2.SupportBundleCollection{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, FileServer: cpv1b2.BundleFileServer{ - URL: url, + URL: url, + HostPublicKey: hostPublicKey, }, Authentication: cpv1b2.BundleServerAuthConfiguration{ BasicAuthentication: &cpv1b2.BasicAuthentication{ diff --git a/pkg/apis/controlplane/types.go b/pkg/apis/controlplane/types.go index 81584ad4ee8..77fe8c2d32e 100644 --- a/pkg/apis/controlplane/types.go +++ b/pkg/apis/controlplane/types.go @@ -560,6 +560,9 @@ type BundleFileServer struct { // The URL of the bundle file server. It is set with format: scheme://host[:port][/path], // e.g, https://api.example.com:8443/v1/supportbundles/. If scheme is not set, https is used by default. URL string + // HostPublicKey specifies the only host public key that will be accepted when connecting to + // the file server. If omitted, any host key will be accepted, which is not recommended. + HostPublicKey []byte } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/controlplane/v1beta2/generated.pb.go b/pkg/apis/controlplane/v1beta2/generated.pb.go index 118fbfa3b43..9c576280e44 100644 --- a/pkg/apis/controlplane/v1beta2/generated.pb.go +++ b/pkg/apis/controlplane/v1beta2/generated.pb.go @@ -1477,198 +1477,200 @@ func init() { } var fileDescriptor_fbaa7d016762fa1d = []byte{ - // 3049 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x1b, 0x4b, 0x6c, 0x24, 0x47, - 0x75, 0x7b, 0x3e, 0xb6, 0xe7, 0x8d, 0xed, 0xf5, 0x96, 0x93, 0xac, 0x49, 0xb2, 0xf6, 0xa6, 0x03, - 0xd1, 0x82, 0xc2, 0x38, 0x36, 0x49, 0x76, 0xc9, 0x4f, 0x78, 0xbc, 0x5e, 0x67, 0x88, 0xed, 0x9d, - 0x94, 0x9d, 0x44, 0x24, 0x24, 0xa4, 0xdd, 0x5d, 0x33, 0x6e, 0xb6, 0xa7, 0xbb, 0xb7, 0xaa, 0xc6, - 0x59, 0xe7, 0x80, 0x82, 0x80, 0x43, 0xf8, 0x05, 0x71, 0x41, 0xb9, 0x71, 0xe3, 0xc2, 0x8d, 0x5b, - 0x4e, 0xe4, 0x80, 0x94, 0x63, 0x10, 0x42, 0xe4, 0x64, 0xb1, 0x46, 0x04, 0xe5, 0x10, 0x21, 0x71, - 0x63, 0x11, 0x12, 0xaa, 0x4f, 0xff, 0xe6, 0xb3, 0xde, 0xb1, 0xbd, 0x06, 0x91, 0x3d, 0x79, 0xfa, - 0xbd, 0x57, 0xef, 0xbd, 0xaa, 0xf7, 0x5e, 0xbd, 0x4f, 0xb7, 0xe1, 0x19, 0xcb, 0xe7, 0x94, 0x58, - 0x15, 0x37, 0x98, 0x55, 0xbf, 0x66, 0xc3, 0x2b, 0xcd, 0x59, 0x2b, 0x74, 0xd9, 0xac, 0x1d, 0xf8, - 0x9c, 0x06, 0x5e, 0xe8, 0x59, 0x3e, 0x99, 0xdd, 0x9e, 0xdb, 0x24, 0xdc, 0x9a, 0x9f, 0x6d, 0x12, - 0x9f, 0x50, 0x8b, 0x13, 0xa7, 0x12, 0xd2, 0x80, 0x07, 0xa8, 0xa2, 0x56, 0x7d, 0xcb, 0x0d, 0xf4, - 0xaf, 0x4a, 0x78, 0xa5, 0x59, 0x11, 0xeb, 0x2b, 0xe9, 0xf5, 0x15, 0xbd, 0xfe, 0xde, 0x0b, 0xfd, - 0xe5, 0x31, 0x6e, 0x71, 0x36, 0xbb, 0x3d, 0x67, 0x79, 0xe1, 0x96, 0x35, 0xd7, 0x29, 0xe9, 0xde, - 0x2f, 0x37, 0x5d, 0xbe, 0xd5, 0xde, 0xac, 0xd8, 0x41, 0x6b, 0xb6, 0x19, 0x34, 0x83, 0x59, 0x09, - 0xde, 0x6c, 0x37, 0xe4, 0x93, 0x7c, 0x90, 0xbf, 0x34, 0xf9, 0xa3, 0x57, 0x2e, 0x30, 0x29, 0x25, - 0x74, 0x5b, 0x96, 0xbd, 0xe5, 0xfa, 0x84, 0xee, 0x24, 0xb2, 0x5a, 0x84, 0x5b, 0xb3, 0xdb, 0xdd, - 0x42, 0x66, 0xfb, 0xad, 0xa2, 0x6d, 0x9f, 0xbb, 0x2d, 0xd2, 0xb5, 0xe0, 0xf1, 0xfd, 0x16, 0x30, - 0x7b, 0x8b, 0xb4, 0xac, 0xae, 0x75, 0x5f, 0xe9, 0xb7, 0xae, 0xcd, 0x5d, 0x6f, 0xd6, 0xf5, 0x39, - 0xe3, 0xb4, 0x73, 0x91, 0xf9, 0x37, 0x03, 0x46, 0x17, 0x1c, 0x87, 0x12, 0xc6, 0x96, 0x69, 0xd0, - 0x0e, 0xd1, 0xeb, 0x30, 0x22, 0x76, 0xe2, 0x58, 0xdc, 0x9a, 0x32, 0xce, 0x1a, 0xe7, 0xca, 0xf3, - 0x8f, 0x54, 0x14, 0xe3, 0x4a, 0x9a, 0x71, 0x62, 0x13, 0x41, 0x5d, 0xd9, 0x9e, 0xab, 0x5c, 0xde, - 0xfc, 0x36, 0xb1, 0xf9, 0x2a, 0xe1, 0x56, 0x15, 0x7d, 0xb0, 0x3b, 0x73, 0x62, 0x6f, 0x77, 0x06, - 0x12, 0x18, 0x8e, 0xb9, 0xa2, 0x36, 0x8c, 0x36, 0x85, 0xa8, 0x55, 0xd2, 0xda, 0x24, 0x94, 0x4d, - 0xe5, 0xce, 0xe6, 0xcf, 0x95, 0xe7, 0x9f, 0x1c, 0xd0, 0xec, 0x95, 0xe5, 0x84, 0x47, 0xf5, 0x2e, - 0x2d, 0x70, 0x34, 0x05, 0x64, 0x38, 0x23, 0xc6, 0xfc, 0x83, 0x01, 0x13, 0xe9, 0x9d, 0xae, 0xb8, - 0x8c, 0xa3, 0x6f, 0x76, 0xed, 0xb6, 0x72, 0x6b, 0xbb, 0x15, 0xab, 0xe5, 0x5e, 0x27, 0xb4, 0xe8, - 0x91, 0x08, 0x92, 0xda, 0xa9, 0x05, 0x45, 0x97, 0x93, 0x56, 0xb4, 0xc5, 0xa7, 0x06, 0xdd, 0x62, - 0x5a, 0xdd, 0xea, 0x98, 0x16, 0x54, 0xac, 0x09, 0x96, 0x58, 0x71, 0x36, 0xdf, 0xce, 0xc3, 0xa9, - 0x34, 0x59, 0xdd, 0xe2, 0xf6, 0xd6, 0x31, 0x18, 0xf1, 0xfb, 0x06, 0x9c, 0xb2, 0x1c, 0x87, 0x38, - 0xcb, 0x47, 0x6c, 0xca, 0xcf, 0x69, 0xb1, 0x62, 0x57, 0x59, 0xee, 0xb8, 0x5b, 0x20, 0xfa, 0xa1, - 0x01, 0x93, 0x94, 0xb4, 0x82, 0xed, 0x0e, 0x45, 0xf2, 0x87, 0x57, 0xe4, 0x3e, 0xad, 0xc8, 0x24, - 0xee, 0xe6, 0x8f, 0x7b, 0x09, 0x35, 0x3f, 0x31, 0x60, 0x7c, 0x21, 0x0c, 0x3d, 0x97, 0x38, 0x1b, - 0xc1, 0xff, 0x79, 0x34, 0xfd, 0xc9, 0x00, 0x94, 0xdd, 0xeb, 0x31, 0xc4, 0x93, 0x9d, 0x8d, 0xa7, - 0x67, 0x06, 0x8e, 0xa7, 0x8c, 0xc2, 0x7d, 0x22, 0xea, 0x47, 0x79, 0x98, 0xcc, 0x12, 0xde, 0x89, - 0xa9, 0xff, 0x5e, 0x4c, 0x5d, 0x85, 0xc9, 0xaa, 0xc5, 0x5c, 0x7b, 0xa1, 0xcd, 0xb7, 0x88, 0xcf, - 0x5d, 0xdb, 0xe2, 0x6e, 0xe0, 0xa3, 0x87, 0x61, 0xa4, 0xcd, 0x08, 0xf5, 0xad, 0x16, 0x91, 0xc6, - 0x28, 0x25, 0x7e, 0xf3, 0x82, 0x86, 0xe3, 0x98, 0x42, 0x50, 0x87, 0x16, 0x63, 0x6f, 0x04, 0xd4, - 0x99, 0xca, 0x65, 0xa9, 0xeb, 0x1a, 0x8e, 0x63, 0x0a, 0x73, 0x0e, 0x26, 0xaa, 0x6d, 0xdf, 0xf1, - 0xc8, 0x25, 0xd7, 0x23, 0xeb, 0x84, 0x6e, 0x13, 0x8a, 0xce, 0x40, 0xbe, 0x4d, 0x3d, 0x2d, 0xaa, - 0xac, 0x17, 0xe7, 0x5f, 0xc0, 0x2b, 0x58, 0xc0, 0xcd, 0x77, 0x72, 0x70, 0x46, 0xad, 0x51, 0xf4, - 0x42, 0xdb, 0xc5, 0xc0, 0x6f, 0xb8, 0xcd, 0x36, 0x55, 0x0a, 0x3f, 0x06, 0xe5, 0x4d, 0x62, 0x51, - 0x42, 0x37, 0x82, 0x2b, 0xc4, 0xd7, 0x8c, 0x26, 0x35, 0xa3, 0x72, 0x35, 0x41, 0xe1, 0x34, 0x1d, - 0x7a, 0x08, 0x86, 0xac, 0xd0, 0x7d, 0x8e, 0xec, 0x68, 0xbd, 0xc7, 0xf5, 0x8a, 0xa1, 0x85, 0x7a, - 0xed, 0x39, 0xb2, 0x83, 0x35, 0x16, 0xfd, 0xd4, 0x80, 0xc9, 0xcd, 0xee, 0x73, 0x9a, 0xca, 0x4b, - 0x47, 0x5d, 0x1c, 0xd4, 0x66, 0x3d, 0x8e, 0xbc, 0x7a, 0x5a, 0xd8, 0xad, 0x07, 0x02, 0xf7, 0x12, - 0x6c, 0xfe, 0xb2, 0x00, 0x93, 0x8b, 0x5e, 0x9b, 0x71, 0x42, 0x33, 0xce, 0x75, 0xfb, 0xa3, 0xe8, - 0xbb, 0x06, 0x4c, 0x90, 0x46, 0x83, 0xd8, 0xdc, 0xdd, 0x26, 0x47, 0x18, 0x44, 0x53, 0x5a, 0xea, - 0xc4, 0x52, 0x07, 0x73, 0xdc, 0x25, 0x0e, 0x7d, 0x07, 0x4e, 0xc5, 0xb0, 0x5a, 0xbd, 0xea, 0x05, - 0xf6, 0x95, 0x28, 0x7e, 0x1e, 0x1b, 0x54, 0x87, 0x5a, 0x7d, 0x8d, 0xf0, 0x24, 0x84, 0x97, 0x3a, - 0xf9, 0xe2, 0x6e, 0x51, 0xe8, 0x02, 0x8c, 0xf2, 0x80, 0x5b, 0x5e, 0xb4, 0xfd, 0xc2, 0x59, 0xe3, - 0x5c, 0x3e, 0xb9, 0xd7, 0x37, 0x52, 0x38, 0x9c, 0xa1, 0x44, 0xf3, 0x00, 0xf2, 0xb9, 0x6e, 0x35, - 0x09, 0x9b, 0x2a, 0xca, 0x75, 0xf1, 0x79, 0x6f, 0xc4, 0x18, 0x9c, 0xa2, 0x12, 0xbe, 0x6d, 0xb7, - 0x29, 0x25, 0x3e, 0x17, 0xcf, 0x53, 0x43, 0x72, 0x51, 0xec, 0xdb, 0x8b, 0x09, 0x0a, 0xa7, 0xe9, - 0xcc, 0x8f, 0x0d, 0x28, 0x2f, 0x35, 0x3f, 0x03, 0x95, 0xe7, 0xef, 0x0d, 0x38, 0x99, 0xda, 0xe8, - 0x31, 0x24, 0xca, 0xd7, 0xb3, 0x89, 0x72, 0xe0, 0x1d, 0xa6, 0xb4, 0xed, 0x93, 0x25, 0x7f, 0x9c, - 0x87, 0x89, 0x14, 0x95, 0x4a, 0x91, 0x0e, 0x40, 0x10, 0x9f, 0xfb, 0x91, 0xda, 0x30, 0xc5, 0xf7, - 0x4e, 0x9a, 0xec, 0x91, 0x26, 0x2d, 0x18, 0x5a, 0xf2, 0xb9, 0xcb, 0x77, 0xd0, 0x4b, 0x90, 0x0f, - 0x03, 0x47, 0x1f, 0xfe, 0xc0, 0x1d, 0x47, 0x3d, 0x70, 0x30, 0x69, 0x10, 0x4a, 0x7c, 0x9b, 0x54, - 0x87, 0x45, 0x8e, 0x13, 0x10, 0xc1, 0xd1, 0xf4, 0xe0, 0xf4, 0xd2, 0x35, 0x2e, 0x32, 0xaa, 0xa7, - 0x44, 0xc5, 0x84, 0xe8, 0x2c, 0x14, 0x52, 0x99, 0x78, 0x54, 0x6b, 0x5f, 0x58, 0x13, 0x59, 0x58, - 0x62, 0xd0, 0x2c, 0x94, 0xc4, 0x5f, 0x16, 0x5a, 0x36, 0xd1, 0xa9, 0xec, 0x94, 0x26, 0x2b, 0xad, - 0x45, 0x08, 0x9c, 0xd0, 0x98, 0xff, 0x32, 0x60, 0x42, 0xee, 0x70, 0x81, 0xb1, 0xc0, 0x76, 0x55, - 0x12, 0x3d, 0x96, 0x12, 0x6c, 0xc2, 0xd2, 0x12, 0xf5, 0x11, 0x1f, 0xb8, 0xda, 0x94, 0xab, 0x93, - 0xd3, 0x8c, 0xf3, 0xc7, 0x42, 0x07, 0x7f, 0xdc, 0x25, 0xd1, 0x7c, 0xaf, 0x00, 0xe5, 0x94, 0x7d, - 0x6f, 0x9b, 0x51, 0xd1, 0xf7, 0x0c, 0x18, 0x27, 0x19, 0xab, 0x4a, 0xeb, 0x94, 0xe7, 0x97, 0x07, - 0xbe, 0x32, 0x7a, 0xfb, 0x46, 0x15, 0xed, 0xed, 0xce, 0x8c, 0x77, 0x20, 0x3b, 0x44, 0xa2, 0x87, - 0x20, 0xef, 0x86, 0x2a, 0x72, 0x46, 0xab, 0x77, 0x09, 0x05, 0x6b, 0x75, 0x76, 0x63, 0x77, 0xa6, - 0x54, 0xab, 0xeb, 0xde, 0x16, 0x0b, 0x02, 0xf4, 0x1a, 0x14, 0xc3, 0x80, 0x72, 0x91, 0xcf, 0x84, - 0x45, 0xbe, 0x3a, 0xa8, 0x8e, 0xc2, 0xd3, 0x9c, 0x7a, 0x40, 0x79, 0x72, 0xa9, 0x89, 0x27, 0x86, - 0x15, 0x5b, 0xf4, 0x0a, 0x14, 0xfc, 0xc0, 0x21, 0x32, 0xed, 0x95, 0xe7, 0x9f, 0x1e, 0x98, 0x7d, - 0xe0, 0x90, 0x64, 0xe3, 0x23, 0x32, 0x04, 0x04, 0x48, 0x32, 0x45, 0x4d, 0x18, 0x66, 0x84, 0x6e, - 0xbb, 0xb6, 0xca, 0x90, 0xe5, 0xf9, 0xaf, 0x0d, 0xca, 0x7f, 0x5d, 0x2d, 0x4f, 0x44, 0x94, 0xf7, - 0x76, 0x67, 0x86, 0x23, 0x68, 0xc4, 0xdd, 0x7c, 0xb7, 0x00, 0xa3, 0x77, 0x6a, 0xae, 0x3b, 0x35, - 0x57, 0xaf, 0x9a, 0xeb, 0x57, 0x06, 0x8c, 0x67, 0xef, 0xa5, 0xec, 0xd5, 0x6c, 0xec, 0x7f, 0x35, - 0xc7, 0xb7, 0x7d, 0xae, 0xef, 0x6d, 0x5f, 0x85, 0x7c, 0xdb, 0x75, 0x64, 0xf3, 0x51, 0xaa, 0x3e, - 0x12, 0x77, 0x4b, 0xb5, 0x8b, 0x37, 0x76, 0x67, 0x1e, 0xe8, 0x37, 0xa5, 0xe4, 0x3b, 0x21, 0x61, - 0x95, 0x17, 0x6a, 0x17, 0xb1, 0x58, 0x6c, 0xbe, 0x09, 0xa3, 0xcf, 0x6e, 0x6c, 0xd4, 0xeb, 0x34, - 0xe0, 0x81, 0x1d, 0x78, 0x42, 0xea, 0x56, 0xc0, 0x78, 0x67, 0x8e, 0x79, 0x36, 0x60, 0x1c, 0x4b, - 0x8c, 0xe8, 0x95, 0x5a, 0x84, 0x6f, 0x05, 0x4e, 0x67, 0xaf, 0xb4, 0x2a, 0xa1, 0x58, 0x63, 0x05, - 0xa7, 0xd0, 0xe2, 0x5b, 0x5a, 0xbd, 0x98, 0x53, 0xdd, 0xe2, 0x5b, 0x58, 0x62, 0xcc, 0xf7, 0x0d, - 0x18, 0xd6, 0x76, 0x45, 0x2f, 0x41, 0xc1, 0x76, 0x1d, 0xaa, 0x03, 0xe7, 0x80, 0x9e, 0x14, 0x0b, - 0x59, 0xac, 0x5d, 0xc4, 0x58, 0x32, 0x44, 0xaf, 0xc2, 0x10, 0xb9, 0x66, 0x93, 0x90, 0xeb, 0x40, - 0x39, 0x20, 0xeb, 0x78, 0x97, 0x4b, 0x92, 0x19, 0xd6, 0x4c, 0xcd, 0x7f, 0x1b, 0x80, 0x6a, 0xf5, - 0xcf, 0x6e, 0x0a, 0x6d, 0x40, 0x51, 0x1e, 0x10, 0x7a, 0x10, 0x72, 0x6e, 0x28, 0xf7, 0x3a, 0x5a, - 0x9d, 0xdc, 0xdb, 0x9d, 0xc9, 0xd5, 0xea, 0xd9, 0xd4, 0x92, 0x73, 0x43, 0x11, 0xbc, 0x21, 0x25, - 0x0d, 0xf7, 0xda, 0x0a, 0xf1, 0x9b, 0x7c, 0x4b, 0x7a, 0x50, 0x31, 0x09, 0xde, 0x7a, 0x0a, 0x87, - 0x33, 0x94, 0xe6, 0x6f, 0x0d, 0x80, 0x95, 0xf3, 0xb1, 0x9b, 0xbe, 0x0c, 0x85, 0x2d, 0xce, 0xc3, - 0x83, 0xa6, 0xea, 0xb4, 0xcb, 0xab, 0x0c, 0x22, 0x20, 0x58, 0xf2, 0x44, 0x2f, 0x42, 0x9e, 0x7b, - 0x4c, 0x27, 0xe8, 0x81, 0xef, 0xd5, 0x8d, 0x95, 0xf5, 0x98, 0xb3, 0x2c, 0x02, 0x36, 0x56, 0xd6, - 0xb1, 0x60, 0x68, 0xbe, 0x6b, 0x00, 0x5a, 0x6d, 0x7b, 0xa2, 0x77, 0x67, 0x5c, 0x1e, 0x5f, 0xcd, - 0x6f, 0x04, 0xe8, 0x41, 0x28, 0xca, 0x36, 0x46, 0x87, 0x5c, 0x9c, 0x32, 0x95, 0x51, 0x14, 0x0e, - 0xbd, 0x06, 0x85, 0x30, 0x70, 0x0e, 0x3c, 0xe1, 0xce, 0x94, 0x26, 0x49, 0x28, 0x06, 0x0e, 0xc3, - 0x92, 0xaf, 0xf9, 0xb6, 0x01, 0xa5, 0x38, 0x6d, 0xcb, 0xd0, 0x0d, 0xa8, 0xba, 0x04, 0x8a, 0x69, - 0x7a, 0xca, 0xb1, 0xc4, 0xdc, 0xc2, 0xe5, 0x74, 0x01, 0x46, 0x42, 0x7d, 0x0e, 0xfa, 0x0a, 0xb8, - 0x3f, 0x1e, 0x06, 0x69, 0xf8, 0x8d, 0xd4, 0x6f, 0x1c, 0x53, 0x9b, 0x9f, 0xe6, 0x61, 0x6c, 0x8d, - 0xf0, 0x37, 0x02, 0x7a, 0xa5, 0x1e, 0x78, 0xae, 0xbd, 0x73, 0x0c, 0xd1, 0xd4, 0x80, 0x22, 0x6d, - 0x7b, 0x24, 0x3a, 0xe0, 0x85, 0x81, 0x6b, 0x92, 0xb4, 0xbe, 0xb8, 0xed, 0x91, 0xc4, 0x8e, 0xe2, - 0x89, 0x61, 0xc5, 0x1e, 0x3d, 0x0d, 0x27, 0xad, 0xcc, 0xd0, 0x53, 0xe5, 0xce, 0x92, 0x0c, 0x99, - 0x93, 0xd9, 0x79, 0x28, 0xc3, 0x9d, 0xb4, 0xe8, 0x9c, 0x38, 0x54, 0x37, 0xa0, 0xa2, 0x80, 0x14, - 0x89, 0xcf, 0xa8, 0x8e, 0xaa, 0x03, 0x55, 0x30, 0x1c, 0x63, 0xd1, 0xa3, 0x30, 0xca, 0x5d, 0x42, - 0x23, 0x8c, 0x4c, 0x77, 0xc5, 0xea, 0x84, 0x4c, 0x91, 0x29, 0x38, 0xce, 0x50, 0x21, 0x06, 0x25, - 0x16, 0xb4, 0xa9, 0x2c, 0x7e, 0x74, 0xf9, 0x74, 0xe9, 0x70, 0x47, 0x11, 0x7b, 0xdd, 0x98, 0x48, - 0x74, 0xeb, 0x11, 0x73, 0x9c, 0xc8, 0x31, 0x3f, 0xcd, 0xc1, 0xe9, 0xcc, 0xa2, 0xa5, 0x6d, 0xcb, - 0x6b, 0x77, 0xdf, 0xa3, 0xf9, 0xdb, 0x34, 0xac, 0x18, 0xa6, 0xe4, 0x6a, 0x9b, 0xe8, 0x9c, 0x57, - 0x9e, 0x5f, 0x3b, 0xd4, 0x86, 0x13, 0xdd, 0xb1, 0xe2, 0xaa, 0xaa, 0x47, 0xfd, 0x80, 0x23, 0x59, - 0x68, 0x07, 0x46, 0x28, 0x61, 0x61, 0xe0, 0x33, 0xa2, 0x6f, 0x9a, 0xcb, 0x47, 0x26, 0x57, 0xb1, - 0x55, 0xae, 0x11, 0x3d, 0xe1, 0x58, 0x9c, 0xf9, 0x77, 0x03, 0xa6, 0x6f, 0xae, 0x33, 0x7a, 0x0d, - 0x86, 0x94, 0x7d, 0xf4, 0x99, 0x3c, 0x3e, 0x70, 0x9b, 0x22, 0x3b, 0x8e, 0x24, 0x6b, 0x6a, 0xc3, - 0x6b, 0xae, 0xa8, 0x05, 0x65, 0x87, 0x30, 0xee, 0xfa, 0x6a, 0x7c, 0x9a, 0x3b, 0x94, 0x90, 0xb8, - 0x1c, 0xbb, 0x98, 0xb0, 0xc4, 0x69, 0xfe, 0xe6, 0x6f, 0x72, 0x30, 0xb3, 0xcf, 0x69, 0x89, 0x16, - 0x6d, 0xcc, 0x4f, 0xd3, 0xe8, 0xad, 0x1f, 0x95, 0xff, 0xdf, 0xad, 0xb5, 0xcc, 0x5e, 0x6d, 0x38, - 0x2b, 0x53, 0x54, 0x89, 0xe2, 0xa2, 0xa8, 0xf9, 0x0e, 0xb9, 0xa6, 0xb3, 0x63, 0x5c, 0x25, 0xe2, - 0x08, 0x81, 0x13, 0x1a, 0xf4, 0x0d, 0x28, 0x88, 0x07, 0x1d, 0x1c, 0xe7, 0x07, 0x55, 0x56, 0xf0, - 0xc4, 0xa4, 0x91, 0xdc, 0xe0, 0x12, 0x20, 0x59, 0x9a, 0x7f, 0x34, 0xe0, 0x54, 0x46, 0xd9, 0x63, - 0x98, 0xa8, 0x6d, 0x66, 0x27, 0x6a, 0x4f, 0x1f, 0xea, 0xf0, 0xfb, 0xcc, 0xd4, 0xfe, 0x61, 0x74, - 0xdc, 0x37, 0xa2, 0x7b, 0x5c, 0xe7, 0x16, 0x6f, 0x33, 0xf4, 0x30, 0x8c, 0x88, 0x2e, 0x72, 0xad, - 0xc7, 0x0b, 0x8f, 0x35, 0x0d, 0xc7, 0x31, 0x85, 0xe8, 0x28, 0xf4, 0x8b, 0xfe, 0xc8, 0x8b, 0x53, - 0x1d, 0xc5, 0x72, 0x8c, 0xc1, 0x29, 0x2a, 0xf4, 0x75, 0x40, 0x94, 0x58, 0x9e, 0xfb, 0xa6, 0x7c, - 0xbc, 0x64, 0xb9, 0x5e, 0x9b, 0x2a, 0xf3, 0x8d, 0x54, 0xef, 0xd5, 0x6b, 0x11, 0xee, 0xa2, 0xc0, - 0x3d, 0x56, 0xa1, 0x2f, 0xc2, 0x70, 0x8b, 0x30, 0x26, 0x3a, 0x93, 0x82, 0x54, 0xf6, 0xa4, 0x66, - 0x30, 0xbc, 0xaa, 0xc0, 0x38, 0xc2, 0xcb, 0x17, 0xd8, 0x99, 0x4d, 0xd7, 0x09, 0xa1, 0xe8, 0x3c, - 0x8c, 0x59, 0xa9, 0xb7, 0xda, 0x6c, 0xca, 0x90, 0xc9, 0xe8, 0x94, 0xf0, 0xd3, 0xf4, 0xeb, 0x6e, - 0x86, 0xb3, 0x74, 0x88, 0xc0, 0x88, 0x1b, 0xea, 0xe6, 0x4f, 0x99, 0xea, 0xfc, 0xe0, 0x75, 0xb5, - 0x5c, 0x9f, 0x1c, 0x70, 0xdc, 0xf5, 0xc5, 0xac, 0xd1, 0x0c, 0x14, 0x1b, 0x57, 0x1d, 0x3f, 0x4a, - 0x92, 0x25, 0x61, 0xcb, 0x4b, 0xcf, 0x5f, 0x5c, 0x63, 0x58, 0xc1, 0x11, 0x17, 0x3d, 0x9d, 0x6e, - 0xcd, 0xa3, 0x79, 0xc5, 0xe1, 0x1b, 0xfe, 0x54, 0x57, 0x18, 0xf1, 0xc6, 0x29, 0x39, 0x22, 0x8b, - 0x7b, 0xd6, 0x26, 0xf1, 0x6a, 0x0e, 0x11, 0x57, 0x90, 0x2b, 0xdb, 0xc9, 0xfc, 0xb9, 0x31, 0x95, - 0xc5, 0x57, 0xb2, 0x28, 0xdc, 0x49, 0x6b, 0x7e, 0x6c, 0xc0, 0x3d, 0xbd, 0x6f, 0x09, 0xf4, 0x18, - 0x14, 0x44, 0x83, 0xa6, 0x7d, 0xef, 0x81, 0x28, 0x2a, 0x37, 0x76, 0x42, 0x72, 0x63, 0x77, 0x26, - 0x6b, 0x41, 0x01, 0xc4, 0x92, 0x7c, 0xe0, 0xb9, 0x5f, 0x5c, 0xbf, 0xe5, 0xf7, 0x6b, 0x2e, 0x0b, - 0x87, 0x69, 0x2e, 0xdf, 0x1f, 0xea, 0x70, 0x3a, 0x71, 0xbb, 0xa0, 0xa7, 0xa0, 0xe4, 0xb8, 0x54, - 0xb4, 0xf5, 0x41, 0xf4, 0x86, 0x6e, 0x3a, 0x52, 0xf6, 0x62, 0x84, 0xb8, 0x91, 0x7e, 0xc0, 0xc9, - 0x02, 0x64, 0x43, 0xa1, 0x41, 0x83, 0x96, 0xce, 0x19, 0x87, 0x2b, 0xd4, 0x44, 0x0c, 0x24, 0x9b, - 0xbf, 0x44, 0x83, 0x16, 0x96, 0xcc, 0xd1, 0xab, 0x90, 0xe3, 0x81, 0xbe, 0x53, 0x8f, 0x40, 0x04, - 0x68, 0x11, 0xb9, 0x8d, 0x00, 0xe7, 0x78, 0x20, 0xa2, 0x87, 0x65, 0x7d, 0xf6, 0xfc, 0x01, 0x7d, - 0x36, 0x89, 0x9e, 0xd8, 0x51, 0x63, 0xd6, 0xf2, 0x7d, 0x6c, 0x47, 0xfd, 0x97, 0x94, 0xe0, 0x5d, - 0x15, 0xe3, 0x8b, 0x30, 0x64, 0x29, 0x9b, 0x0c, 0x49, 0x9b, 0x3c, 0x23, 0xdf, 0x7f, 0x46, 0xc6, - 0x78, 0xe4, 0x26, 0x5f, 0x9b, 0x51, 0x47, 0x7f, 0x64, 0x36, 0x27, 0xf3, 0x89, 0x5a, 0x83, 0x35, - 0x37, 0xf4, 0x24, 0x8c, 0x11, 0xdf, 0xda, 0xf4, 0xc8, 0x4a, 0xd0, 0x6c, 0xba, 0x7e, 0x73, 0x6a, - 0x58, 0xde, 0x75, 0x71, 0x3e, 0x5c, 0x4a, 0x23, 0x71, 0x96, 0xb6, 0x57, 0xbd, 0x3c, 0x32, 0x40, - 0xbd, 0x1c, 0xb9, 0x79, 0xa9, 0xaf, 0x9b, 0x5f, 0x85, 0xb2, 0x17, 0xb7, 0x95, 0x6c, 0x0a, 0xa4, - 0x35, 0x9e, 0x18, 0xd4, 0x1a, 0x49, 0x67, 0x9a, 0x54, 0x23, 0x09, 0x8c, 0xe1, 0xb4, 0x0c, 0x61, - 0x16, 0x2f, 0x68, 0xca, 0x5b, 0x62, 0xaa, 0x9c, 0xcd, 0x31, 0x2b, 0x1a, 0x8e, 0x63, 0x0a, 0xf3, - 0x9d, 0x3c, 0xa0, 0x8c, 0x47, 0x89, 0x4c, 0xc5, 0xfe, 0x47, 0xca, 0x95, 0x10, 0x46, 0x39, 0xb5, - 0x1a, 0x0d, 0xd7, 0x96, 0x5a, 0xdd, 0x42, 0x21, 0x27, 0x3f, 0x15, 0xac, 0x44, 0x9f, 0x0a, 0x56, - 0x36, 0x52, 0xab, 0x53, 0x43, 0xbc, 0x14, 0x14, 0x67, 0x24, 0xa0, 0xb7, 0x0c, 0x98, 0x10, 0xd5, - 0x49, 0x9a, 0x44, 0x8f, 0x1f, 0x9f, 0xb8, 0x75, 0xb1, 0xb8, 0x83, 0x43, 0x32, 0xf2, 0xe8, 0xc4, - 0xe0, 0x2e, 0x69, 0xe6, 0x5f, 0x0d, 0x98, 0xec, 0xb2, 0x48, 0xfb, 0x38, 0xe6, 0xbf, 0x1e, 0x14, - 0x45, 0xed, 0x11, 0xa5, 0xdc, 0xe5, 0x43, 0xd9, 0x3a, 0xa9, 0x7a, 0x92, 0x3a, 0x49, 0xc0, 0x18, - 0x56, 0x42, 0xcc, 0x39, 0x18, 0xcb, 0x8c, 0xda, 0xf7, 0x7f, 0xff, 0x64, 0xbe, 0x57, 0x84, 0x89, - 0x88, 0x2f, 0x5b, 0x6f, 0xb7, 0x5a, 0x16, 0x3d, 0x8e, 0xee, 0xfd, 0x07, 0x06, 0x9c, 0x4c, 0x3b, - 0xa6, 0x1b, 0x1f, 0x51, 0xf5, 0x50, 0x47, 0xa4, 0x7c, 0xe3, 0xb4, 0x96, 0x7d, 0x72, 0x2d, 0x2b, - 0x02, 0x77, 0xca, 0x44, 0xbf, 0x36, 0xe0, 0x7e, 0x25, 0x45, 0x7f, 0x93, 0xd1, 0xb1, 0x42, 0x3b, - 0xea, 0x51, 0x28, 0xf5, 0x79, 0xad, 0xd4, 0xfd, 0x0b, 0x37, 0x91, 0x87, 0x6f, 0xaa, 0x0d, 0xfa, - 0x85, 0x01, 0x77, 0x2b, 0x82, 0x4e, 0x3d, 0x0b, 0x47, 0xa6, 0xe7, 0x19, 0xad, 0xe7, 0xdd, 0x0b, - 0xbd, 0x04, 0xe1, 0xde, 0xf2, 0x11, 0x83, 0x52, 0x2b, 0x9a, 0x94, 0xc9, 0xd2, 0xea, 0x00, 0xca, - 0x74, 0x8f, 0xda, 0x92, 0x9a, 0x28, 0xc6, 0xe1, 0x44, 0x8e, 0xf9, 0x2a, 0xdc, 0x55, 0xb7, 0x9a, - 0xba, 0x67, 0x5c, 0x26, 0xfc, 0x72, 0x28, 0x7e, 0x30, 0x35, 0xc8, 0x6e, 0x2a, 0xb7, 0xcf, 0xa7, - 0x07, 0xd9, 0x4d, 0x82, 0x25, 0x06, 0x3d, 0x08, 0x45, 0xcf, 0x6d, 0xb9, 0x5c, 0xb7, 0x00, 0x71, - 0x38, 0xad, 0x08, 0x20, 0x56, 0x38, 0xd3, 0x82, 0xd1, 0xf4, 0x18, 0xee, 0x76, 0xbc, 0xcd, 0x7d, - 0xdf, 0x80, 0x61, 0xdd, 0xd1, 0x1d, 0xb2, 0xca, 0xda, 0x7f, 0xbe, 0x97, 0x94, 0x0b, 0xf9, 0xa3, - 0x2c, 0x17, 0xcc, 0xdf, 0xe5, 0x21, 0x7a, 0xd7, 0x86, 0x1e, 0x4d, 0xcd, 0x10, 0xd5, 0x16, 0xa6, - 0xf6, 0x9f, 0x1f, 0xa2, 0x35, 0x3d, 0xbd, 0xcc, 0xed, 0x73, 0xd7, 0xb4, 0xb9, 0xeb, 0x55, 0xd4, - 0xf7, 0xda, 0x95, 0x9a, 0xcf, 0x2f, 0xd3, 0x75, 0x4e, 0x5d, 0xbf, 0xa9, 0xe6, 0xc1, 0xa9, 0x59, - 0xe7, 0x17, 0x60, 0x98, 0xf8, 0x72, 0x30, 0x2a, 0xb7, 0x5a, 0x54, 0x13, 0x9d, 0x25, 0x05, 0xc2, - 0x11, 0x0e, 0x9d, 0x83, 0x11, 0xd7, 0x6e, 0x85, 0xa2, 0x2a, 0x97, 0x55, 0x73, 0x51, 0x0d, 0x60, - 0x6a, 0x8b, 0xab, 0x75, 0x59, 0xa9, 0xc7, 0xd8, 0x88, 0x72, 0x31, 0x7a, 0x07, 0x9a, 0xa2, 0x14, - 0x30, 0x1c, 0x63, 0x25, 0x65, 0x53, 0xf3, 0x1c, 0x4a, 0x51, 0x2e, 0xc7, 0x3c, 0x35, 0x16, 0x5d, - 0xd0, 0xdf, 0xdc, 0xe8, 0xae, 0x4d, 0x16, 0x59, 0xa5, 0x8e, 0xcf, 0x66, 0xa2, 0x49, 0x7c, 0x86, - 0x52, 0x6c, 0x8f, 0x51, 0x5b, 0x6e, 0x6f, 0x24, 0xd9, 0xde, 0xba, 0x02, 0xe1, 0x08, 0x87, 0x2a, - 0x00, 0x8c, 0xda, 0x7a, 0xd7, 0xb2, 0xa0, 0x2a, 0x56, 0xc7, 0xc5, 0x8d, 0xbc, 0x1e, 0x43, 0x71, - 0x8a, 0xc2, 0x24, 0x30, 0xd1, 0xd9, 0x57, 0xdd, 0x0e, 0x97, 0x7f, 0xa7, 0x00, 0xa7, 0xd7, 0xdb, - 0xa1, 0x30, 0x94, 0xfa, 0x32, 0x70, 0x31, 0xf0, 0x3c, 0xed, 0xc4, 0xb7, 0x3f, 0xf1, 0xbc, 0x02, - 0x25, 0x72, 0x2d, 0x74, 0x29, 0x71, 0x16, 0x22, 0x7f, 0xfb, 0xd2, 0xad, 0x89, 0xd8, 0x70, 0x5b, - 0x24, 0xd9, 0xda, 0x52, 0xc4, 0x04, 0x27, 0xfc, 0xc4, 0x59, 0x30, 0xd7, 0xb7, 0x89, 0x20, 0xd5, - 0x41, 0x16, 0x2f, 0x58, 0x8f, 0x10, 0x38, 0xa1, 0x11, 0xcd, 0x70, 0x23, 0xfe, 0x96, 0x52, 0xfa, - 0xe0, 0x01, 0x9a, 0xe1, 0xce, 0x6f, 0x32, 0x93, 0x13, 0x48, 0x60, 0x38, 0x25, 0x07, 0xfd, 0xc4, - 0x80, 0x71, 0x2b, 0xfb, 0x39, 0xa4, 0x7a, 0xb1, 0xbf, 0x7a, 0x30, 0xd1, 0x7d, 0x3e, 0xed, 0xac, - 0xde, 0xa3, 0xf5, 0x18, 0xef, 0xf8, 0x2e, 0xb2, 0x43, 0xb8, 0xf9, 0x89, 0x01, 0xf7, 0xf5, 0xf1, - 0x88, 0x63, 0x18, 0x60, 0x79, 0xd9, 0x01, 0xd6, 0xc0, 0x25, 0x5a, 0x1f, 0xcd, 0xfb, 0x8c, 0xb2, - 0x7e, 0x9e, 0x83, 0x07, 0xfa, 0xac, 0x38, 0xf0, 0x50, 0xeb, 0x49, 0x18, 0x8b, 0x7e, 0xa7, 0xc3, - 0x30, 0x69, 0x08, 0xd2, 0x48, 0x9c, 0xa5, 0x8d, 0x44, 0xc9, 0x0b, 0x2b, 0xdf, 0x2d, 0x4a, 0x5d, - 0x5a, 0x11, 0x85, 0xf0, 0x70, 0x3b, 0x68, 0x85, 0x1e, 0xe1, 0x44, 0x4d, 0x1a, 0x46, 0x12, 0x0f, - 0x5f, 0x8c, 0x10, 0x38, 0xa1, 0x11, 0x89, 0x96, 0x50, 0x1a, 0x50, 0xe9, 0x61, 0xa9, 0x77, 0x65, - 0x4b, 0x02, 0x88, 0x15, 0xce, 0xfc, 0xa7, 0x01, 0x67, 0xfa, 0x1c, 0xca, 0xb1, 0x55, 0xea, 0xdb, - 0xd9, 0x4a, 0xfd, 0xf9, 0x23, 0x72, 0x83, 0x7d, 0x6b, 0xf6, 0x87, 0xa1, 0x9c, 0x7a, 0x01, 0x89, - 0xce, 0x40, 0x9e, 0xf9, 0x6e, 0xe7, 0xf7, 0xd4, 0xeb, 0x6b, 0x35, 0x2c, 0xe0, 0xd5, 0x8d, 0x0f, - 0xae, 0x4f, 0x9f, 0xf8, 0xf0, 0xfa, 0xf4, 0x89, 0x8f, 0xae, 0x4f, 0x9f, 0x78, 0x6b, 0x6f, 0xda, - 0xf8, 0x60, 0x6f, 0xda, 0xf8, 0x70, 0x6f, 0xda, 0xf8, 0x68, 0x6f, 0xda, 0xf8, 0xf3, 0xde, 0xb4, - 0xf1, 0xb3, 0xbf, 0x4c, 0x9f, 0x78, 0xb9, 0x32, 0xd8, 0x3f, 0x9a, 0xfd, 0x27, 0x00, 0x00, 0xff, - 0xff, 0xb9, 0xae, 0x10, 0x3d, 0x99, 0x36, 0x00, 0x00, + // 3077 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x3b, 0x4b, 0x6c, 0x1c, 0xc7, + 0xb1, 0x9a, 0xfd, 0x90, 0xdc, 0xda, 0x25, 0x45, 0x35, 0x6d, 0x8b, 0xcf, 0xb6, 0x48, 0x79, 0xfc, + 0x9e, 0xa1, 0x17, 0x38, 0x4b, 0x8b, 0xb1, 0x2d, 0xc5, 0x3f, 0x84, 0x4b, 0x51, 0xf4, 0x26, 0x14, + 0xb5, 0x6e, 0xd2, 0x36, 0x62, 0xc7, 0x8e, 0x87, 0x33, 0xbd, 0xcb, 0xb1, 0x66, 0x67, 0x46, 0xdd, + 0x3d, 0xb4, 0xe4, 0x43, 0xe0, 0x20, 0xc9, 0xc1, 0xf9, 0x39, 0xc8, 0x25, 0xf0, 0x2d, 0xb7, 0x5c, + 0x72, 0xcb, 0xcd, 0xa7, 0xf8, 0x10, 0xc0, 0x47, 0x07, 0x41, 0x10, 0x9f, 0x88, 0x98, 0x41, 0x1c, + 0xf8, 0x60, 0x04, 0xc8, 0x2d, 0x0a, 0x02, 0x04, 0xfd, 0x99, 0xdf, 0x2e, 0x57, 0xd4, 0x92, 0x14, + 0x13, 0xc4, 0x3a, 0x71, 0xa7, 0xaa, 0xba, 0xaa, 0xba, 0xab, 0xba, 0xeb, 0xd3, 0x4d, 0x78, 0xc6, + 0xf2, 0x39, 0x25, 0x56, 0xdd, 0x0d, 0xe6, 0xd4, 0xaf, 0xb9, 0xf0, 0x4a, 0x67, 0xce, 0x0a, 0x5d, + 0x36, 0x67, 0x07, 0x3e, 0xa7, 0x81, 0x17, 0x7a, 0x96, 0x4f, 0xe6, 0xb6, 0xce, 0x6e, 0x10, 0x6e, + 0xcd, 0xcf, 0x75, 0x88, 0x4f, 0xa8, 0xc5, 0x89, 0x53, 0x0f, 0x69, 0xc0, 0x03, 0x54, 0x57, 0xa3, + 0xbe, 0xe9, 0x06, 0xfa, 0x57, 0x3d, 0xbc, 0xd2, 0xa9, 0x8b, 0xf1, 0xf5, 0xec, 0xf8, 0xba, 0x1e, + 0x7f, 0xef, 0xf9, 0xc1, 0xf2, 0x18, 0xb7, 0x38, 0x9b, 0xdb, 0x3a, 0x6b, 0x79, 0xe1, 0xa6, 0x75, + 0xb6, 0x57, 0xd2, 0xbd, 0x5f, 0xec, 0xb8, 0x7c, 0x33, 0xda, 0xa8, 0xdb, 0x41, 0x77, 0xae, 0x13, + 0x74, 0x82, 0x39, 0x09, 0xde, 0x88, 0xda, 0xf2, 0x4b, 0x7e, 0xc8, 0x5f, 0x9a, 0xfc, 0xd1, 0x2b, + 0xe7, 0x99, 0x94, 0x12, 0xba, 0x5d, 0xcb, 0xde, 0x74, 0x7d, 0x42, 0xaf, 0xa7, 0xb2, 0xba, 0x84, + 0x5b, 0x73, 0x5b, 0xfd, 0x42, 0xe6, 0x06, 0x8d, 0xa2, 0x91, 0xcf, 0xdd, 0x2e, 0xe9, 0x1b, 0xf0, + 0xf8, 0x5e, 0x03, 0x98, 0xbd, 0x49, 0xba, 0x56, 0xdf, 0xb8, 0x2f, 0x0d, 0x1a, 0x17, 0x71, 0xd7, + 0x9b, 0x73, 0x7d, 0xce, 0x38, 0xed, 0x1d, 0x64, 0xfe, 0xc5, 0x80, 0xda, 0x82, 0xe3, 0x50, 0xc2, + 0xd8, 0x32, 0x0d, 0xa2, 0x10, 0xbd, 0x06, 0x63, 0x62, 0x26, 0x8e, 0xc5, 0xad, 0x69, 0xe3, 0xb4, + 0x71, 0xa6, 0x3a, 0xff, 0x48, 0x5d, 0x31, 0xae, 0x67, 0x19, 0xa7, 0x36, 0x11, 0xd4, 0xf5, 0xad, + 0xb3, 0xf5, 0xcb, 0x1b, 0xaf, 0x13, 0x9b, 0x5f, 0x22, 0xdc, 0x6a, 0xa0, 0x0f, 0xb6, 0x67, 0x8f, + 0xed, 0x6c, 0xcf, 0x42, 0x0a, 0xc3, 0x09, 0x57, 0x14, 0x41, 0xad, 0x23, 0x44, 0x5d, 0x22, 0xdd, + 0x0d, 0x42, 0xd9, 0x74, 0xe1, 0x74, 0xf1, 0x4c, 0x75, 0xfe, 0xc9, 0x21, 0xcd, 0x5e, 0x5f, 0x4e, + 0x79, 0x34, 0xee, 0xd2, 0x02, 0x6b, 0x19, 0x20, 0xc3, 0x39, 0x31, 0xe6, 0xef, 0x0c, 0x98, 0xcc, + 0xce, 0x74, 0xc5, 0x65, 0x1c, 0x7d, 0xa3, 0x6f, 0xb6, 0xf5, 0x5b, 0x9b, 0xad, 0x18, 0x2d, 0xe7, + 0x3a, 0xa9, 0x45, 0x8f, 0xc5, 0x90, 0xcc, 0x4c, 0x2d, 0x28, 0xbb, 0x9c, 0x74, 0xe3, 0x29, 0x3e, + 0x35, 0xec, 0x14, 0xb3, 0xea, 0x36, 0xc6, 0xb5, 0xa0, 0x72, 0x53, 0xb0, 0xc4, 0x8a, 0xb3, 0xf9, + 0x76, 0x11, 0x4e, 0x64, 0xc9, 0x5a, 0x16, 0xb7, 0x37, 0x8f, 0xc0, 0x88, 0xdf, 0x35, 0xe0, 0x84, + 0xe5, 0x38, 0xc4, 0x59, 0x3e, 0x64, 0x53, 0xfe, 0x8f, 0x16, 0x2b, 0x66, 0x95, 0xe7, 0x8e, 0xfb, + 0x05, 0xa2, 0xef, 0x1b, 0x30, 0x45, 0x49, 0x37, 0xd8, 0xea, 0x51, 0xa4, 0x78, 0x70, 0x45, 0xee, + 0xd3, 0x8a, 0x4c, 0xe1, 0x7e, 0xfe, 0x78, 0x37, 0xa1, 0xe6, 0xa7, 0x06, 0x4c, 0x2c, 0x84, 0xa1, + 0xe7, 0x12, 0x67, 0x3d, 0xf8, 0x2f, 0xdf, 0x4d, 0x7f, 0x30, 0x00, 0xe5, 0xe7, 0x7a, 0x04, 0xfb, + 0xc9, 0xce, 0xef, 0xa7, 0x67, 0x86, 0xde, 0x4f, 0x39, 0x85, 0x07, 0xec, 0xa8, 0x1f, 0x14, 0x61, + 0x2a, 0x4f, 0x78, 0x67, 0x4f, 0xfd, 0xfb, 0xf6, 0xd4, 0x55, 0x98, 0x6a, 0x58, 0xcc, 0xb5, 0x17, + 0x22, 0xbe, 0x49, 0x7c, 0xee, 0xda, 0x16, 0x77, 0x03, 0x1f, 0x3d, 0x0c, 0x63, 0x11, 0x23, 0xd4, + 0xb7, 0xba, 0x44, 0x1a, 0xa3, 0x92, 0xfa, 0xcd, 0xf3, 0x1a, 0x8e, 0x13, 0x0a, 0x41, 0x1d, 0x5a, + 0x8c, 0xbd, 0x11, 0x50, 0x67, 0xba, 0x90, 0xa7, 0x6e, 0x69, 0x38, 0x4e, 0x28, 0xcc, 0xd7, 0x61, + 0xb2, 0x11, 0xf9, 0x8e, 0x47, 0x2e, 0xba, 0x1e, 0x59, 0x23, 0x74, 0x8b, 0x50, 0x74, 0x0a, 0x8a, + 0x11, 0xf5, 0xb4, 0xa8, 0xaa, 0x1e, 0x5c, 0x7c, 0x1e, 0xaf, 0x60, 0x01, 0x47, 0xe7, 0x60, 0x7c, + 0x33, 0x60, 0xbc, 0x15, 0x6d, 0x78, 0xae, 0xfd, 0x35, 0x72, 0x5d, 0x4a, 0xa9, 0x35, 0x4e, 0xec, + 0x6c, 0xcf, 0x8e, 0x3f, 0x9b, 0x45, 0xe0, 0x3c, 0x9d, 0xf9, 0x4e, 0x01, 0x4e, 0x29, 0x61, 0x4a, + 0x90, 0x98, 0xe6, 0x62, 0xe0, 0xb7, 0xdd, 0x4e, 0x44, 0xd5, 0x4c, 0x1f, 0x83, 0xea, 0x06, 0xb1, + 0x28, 0xa1, 0xeb, 0xc1, 0x15, 0xe2, 0x6b, 0x0d, 0xa6, 0xb4, 0x06, 0xd5, 0x46, 0x8a, 0xc2, 0x59, + 0x3a, 0xf4, 0x10, 0x8c, 0x58, 0xa1, 0x1b, 0xab, 0x52, 0x69, 0x4c, 0xe8, 0x11, 0x23, 0x0b, 0xad, + 0xa6, 0xd0, 0x43, 0x63, 0xd1, 0x8f, 0x0d, 0x98, 0xda, 0xe8, 0x5f, 0xe0, 0xe9, 0xa2, 0xf4, 0xf0, + 0xc5, 0x61, 0x8d, 0xbd, 0x8b, 0xad, 0x1a, 0x27, 0x85, 0xc1, 0x77, 0x41, 0xe0, 0xdd, 0x04, 0x9b, + 0x3f, 0x2f, 0xc1, 0xd4, 0xa2, 0x17, 0x31, 0x4e, 0x68, 0xce, 0x2b, 0x6f, 0xff, 0xf6, 0xfb, 0xb6, + 0x01, 0x93, 0xa4, 0xdd, 0x26, 0x36, 0x77, 0xb7, 0xc8, 0x21, 0xee, 0xbe, 0x69, 0x2d, 0x75, 0x72, + 0xa9, 0x87, 0x39, 0xee, 0x13, 0x87, 0xbe, 0x05, 0x27, 0x12, 0x58, 0xb3, 0xd5, 0xf0, 0x02, 0xfb, + 0x4a, 0xbc, 0xf1, 0x1e, 0x1b, 0x56, 0x87, 0x66, 0x6b, 0x95, 0xf0, 0x74, 0xef, 0x2f, 0xf5, 0xf2, + 0xc5, 0xfd, 0xa2, 0xd0, 0x79, 0xa8, 0xf1, 0x80, 0x5b, 0x5e, 0x3c, 0xfd, 0xd2, 0x69, 0xe3, 0x4c, + 0x31, 0x0d, 0x08, 0xeb, 0x19, 0x1c, 0xce, 0x51, 0xa2, 0x79, 0x00, 0xf9, 0xdd, 0xb2, 0x3a, 0x84, + 0x4d, 0x97, 0xe5, 0xb8, 0x64, 0xbd, 0xd7, 0x13, 0x0c, 0xce, 0x50, 0x09, 0xdf, 0xb6, 0x23, 0x4a, + 0x89, 0xcf, 0xc5, 0xf7, 0xf4, 0x88, 0x1c, 0x94, 0xf8, 0xf6, 0x62, 0x8a, 0xc2, 0x59, 0x3a, 0xf3, + 0x13, 0x03, 0xaa, 0x4b, 0x9d, 0xcf, 0x41, 0xca, 0xfa, 0x5b, 0x03, 0x8e, 0x67, 0x26, 0x7a, 0x04, + 0x11, 0xf6, 0xb5, 0x7c, 0x84, 0x1d, 0x7a, 0x86, 0x19, 0x6d, 0x07, 0x84, 0xd7, 0x1f, 0x16, 0x61, + 0x32, 0x43, 0xa5, 0x62, 0xab, 0x03, 0x10, 0x24, 0xeb, 0x7e, 0xa8, 0x36, 0xcc, 0xf0, 0xbd, 0x13, + 0x5f, 0x77, 0x89, 0xaf, 0x16, 0x8c, 0x2c, 0xf9, 0xdc, 0xe5, 0xd7, 0xd1, 0x8b, 0x50, 0x0c, 0x03, + 0x47, 0x2f, 0xfe, 0xd0, 0xa5, 0x4a, 0x2b, 0x70, 0x30, 0x69, 0x13, 0x4a, 0x7c, 0x9b, 0x34, 0x46, + 0x45, 0x70, 0x14, 0x10, 0xc1, 0xd1, 0xf4, 0xe0, 0xe4, 0xd2, 0x35, 0x2e, 0x42, 0xb1, 0xa7, 0x44, + 0x25, 0x84, 0xe8, 0x34, 0x94, 0x32, 0x21, 0xbc, 0xa6, 0xb5, 0x2f, 0xad, 0x8a, 0xf0, 0x2d, 0x31, + 0x68, 0x0e, 0x2a, 0xe2, 0x2f, 0x0b, 0x2d, 0x9b, 0xe8, 0x50, 0x76, 0x42, 0x93, 0x55, 0x56, 0x63, + 0x04, 0x4e, 0x69, 0xcc, 0x7f, 0x18, 0x30, 0x29, 0x67, 0xb8, 0xc0, 0x58, 0x60, 0xbb, 0x2a, 0x88, + 0x1e, 0x49, 0xee, 0x36, 0x69, 0x69, 0x89, 0x7a, 0x89, 0xf7, 0x9d, 0xa6, 0xca, 0xd1, 0xe9, 0x6a, + 0x26, 0xf1, 0x63, 0xa1, 0x87, 0x3f, 0xee, 0x93, 0x68, 0xbe, 0x57, 0x82, 0x6a, 0xc6, 0xbe, 0xb7, + 0xcd, 0xa8, 0xe8, 0x3b, 0x06, 0x4c, 0x90, 0x9c, 0x55, 0xa5, 0x75, 0xaa, 0xf3, 0xcb, 0x43, 0x1f, + 0x19, 0xbb, 0xfb, 0x46, 0x03, 0xed, 0x6c, 0xcf, 0x4e, 0xf4, 0x20, 0x7b, 0x44, 0xa2, 0x87, 0xa0, + 0xe8, 0x86, 0x6a, 0xe7, 0xd4, 0x1a, 0x77, 0x09, 0x05, 0x9b, 0x2d, 0x76, 0x63, 0x7b, 0xb6, 0xd2, + 0x6c, 0xe9, 0xa2, 0x18, 0x0b, 0x02, 0xf4, 0x2a, 0x94, 0xc3, 0x80, 0x72, 0x11, 0xcf, 0x84, 0x45, + 0xbe, 0x3c, 0xac, 0x8e, 0xc2, 0xd3, 0x9c, 0x56, 0x40, 0x79, 0x7a, 0xa8, 0x89, 0x2f, 0x86, 0x15, + 0x5b, 0xf4, 0x32, 0x94, 0xfc, 0xc0, 0x21, 0x32, 0xec, 0x55, 0xe7, 0x9f, 0x1e, 0x9a, 0x7d, 0xe0, + 0x90, 0x74, 0xe2, 0x63, 0x72, 0x0b, 0x08, 0x90, 0x64, 0x8a, 0x3a, 0x30, 0xca, 0x08, 0xdd, 0x72, + 0x6d, 0x15, 0x21, 0xab, 0xf3, 0x5f, 0x19, 0x96, 0xff, 0x9a, 0x1a, 0x9e, 0x8a, 0xa8, 0xee, 0x6c, + 0xcf, 0x8e, 0xc6, 0xd0, 0x98, 0xbb, 0xf9, 0x6e, 0x09, 0x6a, 0x77, 0x72, 0xae, 0x3b, 0x39, 0xd7, + 0x6e, 0x39, 0xd7, 0x2f, 0x0c, 0x98, 0xc8, 0x9f, 0x4b, 0xf9, 0xa3, 0xd9, 0xd8, 0xfb, 0x68, 0x4e, + 0x4e, 0xfb, 0xc2, 0xc0, 0xd3, 0xbe, 0x01, 0xc5, 0xc8, 0x75, 0x64, 0xf1, 0x51, 0x69, 0x3c, 0x92, + 0x94, 0x59, 0xcd, 0x0b, 0x37, 0xb6, 0x67, 0x1f, 0x18, 0xd4, 0xde, 0xe4, 0xd7, 0x43, 0xc2, 0xea, + 0xcf, 0x37, 0x2f, 0x60, 0x31, 0xd8, 0x7c, 0x13, 0x6a, 0xcf, 0xae, 0xaf, 0xb7, 0x5a, 0x34, 0xe0, + 0x81, 0x1d, 0x78, 0x42, 0xaa, 0xa8, 0xb9, 0x7a, 0x63, 0x8c, 0x28, 0xcb, 0xb0, 0xc4, 0x88, 0x5a, + 0xa9, 0x4b, 0xf8, 0x66, 0xe0, 0xf4, 0xd6, 0x4a, 0x97, 0x24, 0x14, 0x6b, 0xac, 0xe0, 0x14, 0x5a, + 0x7c, 0x53, 0xab, 0x97, 0x70, 0x6a, 0x59, 0x7c, 0x13, 0x4b, 0x8c, 0xf9, 0xbe, 0x01, 0xa3, 0xda, + 0xae, 0xe8, 0x45, 0x28, 0xd9, 0xae, 0x43, 0xf5, 0xc6, 0xd9, 0xa7, 0x27, 0x25, 0x42, 0x16, 0x9b, + 0x17, 0x30, 0x96, 0x0c, 0xd1, 0x2b, 0x30, 0x42, 0xae, 0xd9, 0x24, 0xe4, 0x7a, 0xa3, 0xec, 0x93, + 0x75, 0x32, 0xcb, 0x25, 0xc9, 0x0c, 0x6b, 0xa6, 0xe6, 0x3f, 0x0d, 0x40, 0xcd, 0xd6, 0xe7, 0x37, + 0x84, 0xb6, 0xa1, 0x2c, 0x17, 0x08, 0x3d, 0x08, 0x05, 0x37, 0x94, 0x73, 0xad, 0x35, 0xa6, 0x76, + 0xb6, 0x67, 0x0b, 0xcd, 0x56, 0x3e, 0xb4, 0x14, 0xdc, 0x50, 0x6c, 0xde, 0x90, 0x92, 0xb6, 0x7b, + 0x6d, 0x85, 0xf8, 0x1d, 0xbe, 0x29, 0x3d, 0xa8, 0x9c, 0x6e, 0xde, 0x56, 0x06, 0x87, 0x73, 0x94, + 0xe6, 0xaf, 0x0d, 0x80, 0x95, 0x73, 0x89, 0x9b, 0xbe, 0x04, 0xa5, 0x4d, 0xce, 0xc3, 0xfd, 0x86, + 0xea, 0xac, 0xcb, 0xab, 0x08, 0x22, 0x20, 0x58, 0xf2, 0x44, 0x2f, 0x40, 0x91, 0x7b, 0x4c, 0x07, + 0xe8, 0xa1, 0xcf, 0xd5, 0xf5, 0x95, 0xb5, 0x84, 0xb3, 0x4c, 0x02, 0xd6, 0x57, 0xd6, 0xb0, 0x60, + 0x68, 0xbe, 0x6b, 0x00, 0xba, 0x14, 0x79, 0xa2, 0x76, 0x67, 0x5c, 0x2e, 0x5f, 0xd3, 0x6f, 0x07, + 0xe8, 0x41, 0x28, 0xcb, 0x32, 0x46, 0x6f, 0xb9, 0x24, 0x64, 0x2a, 0xa3, 0x28, 0x1c, 0x7a, 0x15, + 0x4a, 0x61, 0xe0, 0xec, 0xbb, 0x35, 0x9e, 0x4b, 0x4d, 0xd2, 0xad, 0x18, 0x38, 0x0c, 0x4b, 0xbe, + 0xe6, 0xdb, 0x06, 0x54, 0x92, 0xb0, 0x2d, 0xb7, 0x6e, 0x40, 0xd5, 0x21, 0x50, 0xce, 0xd2, 0x53, + 0x8e, 0x25, 0xe6, 0x16, 0x0e, 0xa7, 0xf3, 0x30, 0x16, 0xea, 0x75, 0xd0, 0x47, 0xc0, 0xfd, 0x49, + 0x17, 0x49, 0xc3, 0x6f, 0x64, 0x7e, 0xe3, 0x84, 0xda, 0xfc, 0xac, 0x08, 0xe3, 0xab, 0x84, 0xbf, + 0x11, 0xd0, 0x2b, 0xad, 0xc0, 0x73, 0xed, 0xeb, 0x47, 0xb0, 0x9b, 0xda, 0x50, 0xa6, 0x91, 0x47, + 0xe2, 0x05, 0x5e, 0x18, 0x3a, 0x27, 0xc9, 0xea, 0x8b, 0x23, 0x8f, 0xa4, 0x76, 0x14, 0x5f, 0x0c, + 0x2b, 0xf6, 0xe8, 0x69, 0x38, 0x6e, 0xe5, 0xba, 0xa5, 0x2a, 0x76, 0x56, 0xe4, 0x96, 0x39, 0x9e, + 0x6f, 0xa4, 0x32, 0xdc, 0x4b, 0x8b, 0xce, 0x88, 0x45, 0x75, 0x03, 0x2a, 0x12, 0x48, 0x11, 0xf8, + 0x8c, 0x46, 0x4d, 0x2d, 0xa8, 0x82, 0xe1, 0x04, 0x8b, 0x1e, 0x85, 0x1a, 0x77, 0x09, 0x8d, 0x31, + 0x32, 0xdc, 0x95, 0x1b, 0x93, 0x32, 0x44, 0x66, 0xe0, 0x38, 0x47, 0x85, 0x18, 0x54, 0x58, 0x10, + 0x51, 0x99, 0xfc, 0xe8, 0xf4, 0xe9, 0xe2, 0xc1, 0x96, 0x22, 0xf1, 0xba, 0x71, 0x11, 0xe8, 0xd6, + 0x62, 0xe6, 0x38, 0x95, 0x63, 0x7e, 0x56, 0x80, 0x93, 0xb9, 0x41, 0x4b, 0x5b, 0x96, 0x17, 0xf5, + 0x9f, 0xa3, 0xc5, 0xdb, 0xd4, 0xac, 0x18, 0xa5, 0xe4, 0x6a, 0x44, 0x74, 0xcc, 0xab, 0xce, 0xaf, + 0x1e, 0x68, 0xc2, 0xa9, 0xee, 0x58, 0x71, 0x55, 0xd9, 0xa3, 0xfe, 0xc0, 0xb1, 0x2c, 0x74, 0x1d, + 0xc6, 0x28, 0x61, 0x61, 0xe0, 0x33, 0xa2, 0x4f, 0x9a, 0xcb, 0x87, 0x26, 0x57, 0xb1, 0x55, 0xae, + 0x11, 0x7f, 0xe1, 0x44, 0x9c, 0xf9, 0x57, 0x03, 0x66, 0x6e, 0xae, 0x33, 0x7a, 0x15, 0x46, 0x94, + 0x7d, 0xf4, 0x9a, 0x3c, 0x3e, 0x74, 0x99, 0x22, 0x2b, 0x8e, 0x34, 0x6a, 0x6a, 0xc3, 0x6b, 0xae, + 0xa8, 0x0b, 0x55, 0x87, 0x30, 0xee, 0xfa, 0xaa, 0x7d, 0x5a, 0x38, 0x90, 0x90, 0x24, 0x1d, 0xbb, + 0x90, 0xb2, 0xc4, 0x59, 0xfe, 0xe6, 0xaf, 0x0a, 0x30, 0xbb, 0xc7, 0x6a, 0x89, 0x12, 0x6d, 0xdc, + 0xcf, 0xd2, 0xe8, 0xa9, 0x1f, 0x96, 0xff, 0xdf, 0xad, 0xb5, 0xcc, 0x1f, 0x6d, 0x38, 0x2f, 0x53, + 0x64, 0x89, 0xe2, 0xa0, 0x68, 0xfa, 0x0e, 0xb9, 0xa6, 0xa3, 0x63, 0x92, 0x25, 0xe2, 0x18, 0x81, + 0x53, 0x1a, 0xf4, 0x75, 0x28, 0x89, 0x0f, 0xbd, 0x39, 0xce, 0x0d, 0xab, 0xac, 0xe0, 0x89, 0x49, + 0x3b, 0x3d, 0xc1, 0x25, 0x40, 0xb2, 0x34, 0x7f, 0x6f, 0xc0, 0x89, 0x9c, 0xb2, 0x47, 0xd0, 0x51, + 0xdb, 0xc8, 0x77, 0xd4, 0x9e, 0x3e, 0xd0, 0xe2, 0x0f, 0xe8, 0xa9, 0xfd, 0xcd, 0xe8, 0x39, 0x6f, + 0x44, 0xf5, 0xb8, 0xc6, 0x2d, 0x1e, 0x31, 0xf4, 0x30, 0x8c, 0x89, 0x2a, 0x72, 0x75, 0x97, 0x9b, + 0x92, 0x55, 0x0d, 0xc7, 0x09, 0x85, 0xa8, 0x28, 0xf4, 0x0b, 0x81, 0xd8, 0x8b, 0x33, 0x15, 0xc5, + 0x72, 0x82, 0xc1, 0x19, 0x2a, 0xf4, 0x55, 0x40, 0x94, 0x58, 0x9e, 0xfb, 0xa6, 0xfc, 0xbc, 0x68, + 0xb9, 0x5e, 0x44, 0x95, 0xf9, 0xc6, 0x1a, 0xf7, 0xea, 0xb1, 0x08, 0xf7, 0x51, 0xe0, 0x5d, 0x46, + 0xa1, 0xff, 0x87, 0xd1, 0x2e, 0x61, 0x4c, 0x54, 0x26, 0x25, 0xa9, 0xec, 0x71, 0xcd, 0x60, 0xf4, + 0x92, 0x02, 0xe3, 0x18, 0x2f, 0x6f, 0xbe, 0x73, 0x93, 0x6e, 0x11, 0x42, 0xd1, 0x39, 0x18, 0xb7, + 0x32, 0xd7, 0xe1, 0x6c, 0xda, 0x90, 0xc1, 0x48, 0xde, 0xc4, 0x64, 0xef, 0xc9, 0x19, 0xce, 0xd3, + 0x21, 0x02, 0x63, 0x6e, 0xa8, 0x8b, 0x3f, 0x65, 0xaa, 0x73, 0xc3, 0xe7, 0xd5, 0x72, 0x7c, 0xba, + 0xc0, 0x49, 0xd5, 0x97, 0xb0, 0x46, 0xb3, 0x50, 0x6e, 0x5f, 0x75, 0xfc, 0x38, 0x48, 0x56, 0x84, + 0x2d, 0x2f, 0x3e, 0x77, 0x61, 0x95, 0x61, 0x05, 0x47, 0x5c, 0xd4, 0x74, 0xba, 0x34, 0x8f, 0xfb, + 0x15, 0x07, 0x2f, 0xf8, 0x33, 0x55, 0x61, 0xcc, 0x1b, 0x67, 0xe4, 0x88, 0x28, 0xee, 0x59, 0x1b, + 0xc4, 0x6b, 0x3a, 0x44, 0x1c, 0x41, 0xae, 0x2c, 0x27, 0x8b, 0x67, 0xc6, 0x55, 0x14, 0x5f, 0xc9, + 0xa3, 0x70, 0x2f, 0xad, 0xf9, 0x89, 0x01, 0xf7, 0xec, 0x7e, 0x4a, 0xa0, 0xc7, 0xa0, 0x24, 0x0a, + 0x34, 0xed, 0x7b, 0x0f, 0xc4, 0xbb, 0x72, 0xfd, 0x7a, 0x48, 0x6e, 0x6c, 0xcf, 0xe6, 0x2d, 0x28, + 0x80, 0x58, 0x92, 0x0f, 0xdd, 0xf7, 0x4b, 0xf2, 0xb7, 0xe2, 0x5e, 0xc5, 0x65, 0xe9, 0x20, 0xc5, + 0xe5, 0xfb, 0x23, 0x3d, 0x4e, 0x27, 0x4e, 0x17, 0xf4, 0x14, 0x54, 0x1c, 0x97, 0x8a, 0xb2, 0x3e, + 0x88, 0x6f, 0xe8, 0x66, 0x62, 0x65, 0x2f, 0xc4, 0x88, 0x1b, 0xd9, 0x0f, 0x9c, 0x0e, 0x40, 0x36, + 0x94, 0xda, 0x34, 0xe8, 0xea, 0x98, 0x71, 0xb0, 0x44, 0x4d, 0xec, 0x81, 0x74, 0xf2, 0x17, 0x69, + 0xd0, 0xc5, 0x92, 0x39, 0x7a, 0x05, 0x0a, 0x3c, 0xd0, 0x67, 0xea, 0x21, 0x88, 0x00, 0x2d, 0xa2, + 0xb0, 0x1e, 0xe0, 0x02, 0x0f, 0xc4, 0xee, 0x61, 0x79, 0x9f, 0x3d, 0xb7, 0x4f, 0x9f, 0x4d, 0x77, + 0x4f, 0xe2, 0xa8, 0x09, 0x6b, 0x79, 0x91, 0xdb, 0x93, 0xff, 0xa5, 0x29, 0x78, 0x5f, 0xc6, 0xf8, + 0x02, 0x8c, 0x58, 0xca, 0x26, 0x23, 0xd2, 0x26, 0xcf, 0xc8, 0xfb, 0xcf, 0xd8, 0x18, 0x8f, 0xdc, + 0xe4, 0x99, 0x1a, 0x75, 0xf4, 0xeb, 0xb4, 0xb3, 0x32, 0x9e, 0xa8, 0x31, 0x58, 0x73, 0x43, 0x4f, + 0xc2, 0x38, 0xf1, 0xad, 0x0d, 0x8f, 0xac, 0x04, 0x9d, 0x8e, 0xeb, 0x77, 0xa6, 0x47, 0xe5, 0x59, + 0x97, 0xc4, 0xc3, 0xa5, 0x2c, 0x12, 0xe7, 0x69, 0x77, 0xcb, 0x97, 0xc7, 0x86, 0xc8, 0x97, 0x63, + 0x37, 0xaf, 0x0c, 0x74, 0xf3, 0xab, 0x50, 0xf5, 0x92, 0xb2, 0x92, 0x4d, 0x83, 0xb4, 0xc6, 0x13, + 0xc3, 0x5a, 0x23, 0xad, 0x4c, 0xd3, 0x6c, 0x24, 0x85, 0x31, 0x9c, 0x95, 0x21, 0xcc, 0xe2, 0x05, + 0x1d, 0x79, 0x4a, 0x4c, 0x57, 0xf3, 0x31, 0x66, 0x45, 0xc3, 0x71, 0x42, 0x61, 0xbe, 0x53, 0x04, + 0x94, 0xf3, 0x28, 0x11, 0xa9, 0xd8, 0x7f, 0x48, 0xba, 0x12, 0x42, 0x8d, 0x53, 0xab, 0xdd, 0x76, + 0x6d, 0xa9, 0xd5, 0x2d, 0x24, 0x72, 0xf2, 0x8d, 0x61, 0x3d, 0x7e, 0x63, 0x58, 0x5f, 0xcf, 0x8c, + 0xce, 0x34, 0xf1, 0x32, 0x50, 0x9c, 0x93, 0x80, 0xde, 0x32, 0x60, 0x52, 0x64, 0x27, 0x59, 0x12, + 0xdd, 0x7e, 0x7c, 0xe2, 0xd6, 0xc5, 0xe2, 0x1e, 0x0e, 0x69, 0xcb, 0xa3, 0x17, 0x83, 0xfb, 0xa4, + 0x99, 0x7f, 0x36, 0x60, 0xaa, 0xcf, 0x22, 0xd1, 0x51, 0xf4, 0x7f, 0x3d, 0x28, 0x8b, 0xdc, 0x23, + 0x0e, 0xb9, 0xcb, 0x07, 0xb2, 0x75, 0x9a, 0xf5, 0xa4, 0x79, 0x92, 0x80, 0x31, 0xac, 0x84, 0x98, + 0x67, 0x61, 0x3c, 0xd7, 0x6a, 0xdf, 0xfb, 0xfe, 0xc9, 0x7c, 0xaf, 0x0c, 0x93, 0x31, 0x5f, 0xb6, + 0x16, 0x75, 0xbb, 0x16, 0x3d, 0x8a, 0xea, 0xfd, 0x7b, 0x06, 0x1c, 0xcf, 0x3a, 0xa6, 0x9b, 0x2c, + 0x51, 0xe3, 0x40, 0x4b, 0xa4, 0x7c, 0xe3, 0xa4, 0x96, 0x7d, 0x7c, 0x35, 0x2f, 0x02, 0xf7, 0xca, + 0x44, 0xbf, 0x34, 0xe0, 0x7e, 0x25, 0x45, 0xbf, 0xc9, 0xe8, 0x19, 0xa1, 0x1d, 0xf5, 0x30, 0x94, + 0xfa, 0x5f, 0xad, 0xd4, 0xfd, 0x0b, 0x37, 0x91, 0x87, 0x6f, 0xaa, 0x0d, 0xfa, 0x99, 0x01, 0x77, + 0x2b, 0x82, 0x5e, 0x3d, 0x4b, 0x87, 0xa6, 0xe7, 0x29, 0xad, 0xe7, 0xdd, 0x0b, 0xbb, 0x09, 0xc2, + 0xbb, 0xcb, 0x47, 0x0c, 0x2a, 0xdd, 0xb8, 0x53, 0x26, 0x53, 0xab, 0x7d, 0x28, 0xd3, 0xdf, 0x6a, + 0x4b, 0x73, 0xa2, 0x04, 0x87, 0x53, 0x39, 0xe6, 0x2b, 0x70, 0x57, 0xcb, 0xea, 0xe8, 0x9a, 0x71, + 0x99, 0xf0, 0xcb, 0xa1, 0xf8, 0xc1, 0x54, 0x23, 0xbb, 0xa3, 0xdc, 0xbe, 0x98, 0x6d, 0x64, 0x77, + 0x08, 0x96, 0x18, 0xf4, 0x20, 0x94, 0x3d, 0xb7, 0xeb, 0x72, 0x5d, 0x02, 0x24, 0xdb, 0x69, 0x45, + 0x00, 0xb1, 0xc2, 0x99, 0x16, 0xd4, 0xb2, 0x6d, 0xb8, 0xdb, 0x71, 0x9b, 0xfb, 0xbe, 0x01, 0xa3, + 0xba, 0xa2, 0x3b, 0x60, 0x96, 0xb5, 0x77, 0x7f, 0x2f, 0x4d, 0x17, 0x8a, 0x87, 0x99, 0x2e, 0x98, + 0xbf, 0x29, 0x42, 0x7c, 0xd7, 0x86, 0x1e, 0xcd, 0xf4, 0x10, 0xd5, 0x14, 0xa6, 0xf7, 0xee, 0x1f, + 0xa2, 0x55, 0xdd, 0xbd, 0x2c, 0xec, 0x71, 0xd6, 0x44, 0xdc, 0xf5, 0xea, 0xea, 0xa1, 0x77, 0xbd, + 0xe9, 0xf3, 0xcb, 0x74, 0x8d, 0x53, 0xd7, 0xef, 0xa8, 0x7e, 0x70, 0xa6, 0xd7, 0xf9, 0x7f, 0x30, + 0x4a, 0x7c, 0xd9, 0x18, 0x95, 0x53, 0x2d, 0xab, 0x8e, 0xce, 0x92, 0x02, 0xe1, 0x18, 0x87, 0xce, + 0xc0, 0x98, 0x6b, 0x77, 0x43, 0x91, 0x95, 0xcb, 0xac, 0xb9, 0xac, 0x1a, 0x30, 0xcd, 0xc5, 0x4b, + 0x2d, 0x99, 0xa9, 0x27, 0xd8, 0x98, 0x72, 0x31, 0xbe, 0x03, 0xcd, 0x50, 0x0a, 0x18, 0x4e, 0xb0, + 0x92, 0xb2, 0xa3, 0x79, 0x8e, 0x64, 0x28, 0x97, 0x13, 0x9e, 0x1a, 0x8b, 0xce, 0xeb, 0x37, 0x37, + 0xba, 0x6a, 0x93, 0x49, 0x56, 0xa5, 0xe7, 0xd9, 0x4c, 0xdc, 0x89, 0xcf, 0x51, 0x8a, 0xe9, 0x31, + 0x6a, 0xcb, 0xe9, 0x8d, 0xa5, 0xd3, 0x5b, 0x53, 0x20, 0x1c, 0xe3, 0x50, 0x1d, 0x80, 0x51, 0x5b, + 0xcf, 0x5a, 0x26, 0x54, 0xe5, 0xc6, 0x84, 0x38, 0x91, 0xd7, 0x12, 0x28, 0xce, 0x50, 0x98, 0x04, + 0x26, 0x7b, 0xeb, 0xaa, 0xdb, 0xe1, 0xf2, 0xef, 0x94, 0xe0, 0xe4, 0x5a, 0x14, 0x0a, 0x43, 0xa9, + 0x97, 0x81, 0x8b, 0x81, 0xe7, 0x69, 0x27, 0xbe, 0xfd, 0x81, 0xe7, 0x65, 0xa8, 0x90, 0x6b, 0xa1, + 0x4b, 0x89, 0xb3, 0x10, 0xfb, 0xdb, 0x17, 0x6e, 0x4d, 0xc4, 0xba, 0xdb, 0x25, 0xe9, 0xd4, 0x96, + 0x62, 0x26, 0x38, 0xe5, 0x27, 0xd6, 0x82, 0xb9, 0xbe, 0x4d, 0x04, 0xa9, 0xde, 0x64, 0xc9, 0x80, + 0xb5, 0x18, 0x81, 0x53, 0x1a, 0x51, 0x0c, 0xb7, 0x93, 0x47, 0x98, 0xd2, 0x07, 0xf7, 0x51, 0x0c, + 0xf7, 0x3e, 0xe6, 0x4c, 0x57, 0x20, 0x85, 0xe1, 0x8c, 0x1c, 0xf4, 0x23, 0x03, 0x26, 0xac, 0xfc, + 0x73, 0x48, 0x75, 0xb1, 0x7f, 0x69, 0x7f, 0xa2, 0x07, 0x3c, 0xed, 0x6c, 0xdc, 0xa3, 0xf5, 0x98, + 0xe8, 0x79, 0x17, 0xd9, 0x23, 0xdc, 0xfc, 0xd4, 0x80, 0xfb, 0x06, 0x78, 0xc4, 0x11, 0x34, 0xb0, + 0xbc, 0x7c, 0x03, 0x6b, 0xe8, 0x14, 0x6d, 0x80, 0xe6, 0x03, 0x5a, 0x59, 0x3f, 0x2d, 0xc0, 0x03, + 0x03, 0x46, 0xec, 0xbb, 0xa9, 0xf5, 0x24, 0x8c, 0xc7, 0xbf, 0xb3, 0xdb, 0x30, 0x2d, 0x08, 0xb2, + 0x48, 0x9c, 0xa7, 0x8d, 0x45, 0xc9, 0x03, 0xab, 0xd8, 0x2f, 0x4a, 0x1d, 0x5a, 0x31, 0x85, 0xf0, + 0x70, 0x3b, 0xe8, 0x86, 0x1e, 0xe1, 0x44, 0x75, 0x1a, 0xc6, 0x52, 0x0f, 0x5f, 0x8c, 0x11, 0x38, + 0xa5, 0x11, 0x81, 0x96, 0x50, 0x1a, 0x50, 0xe9, 0x61, 0x99, 0xbb, 0xb2, 0x25, 0x01, 0xc4, 0x0a, + 0x67, 0xfe, 0xdd, 0x80, 0x53, 0x03, 0x16, 0xe5, 0xc8, 0x32, 0xf5, 0xad, 0x7c, 0xa6, 0xfe, 0xdc, + 0x21, 0xb9, 0xc1, 0x9e, 0x39, 0xfb, 0xc3, 0x50, 0xcd, 0x5c, 0x40, 0xa2, 0x53, 0x50, 0x64, 0xbe, + 0xdb, 0xfb, 0x10, 0x7b, 0x6d, 0xb5, 0x89, 0x05, 0xbc, 0xb1, 0xfe, 0xc1, 0xc7, 0x33, 0xc7, 0x3e, + 0xfc, 0x78, 0xe6, 0xd8, 0x47, 0x1f, 0xcf, 0x1c, 0x7b, 0x6b, 0x67, 0xc6, 0xf8, 0x60, 0x67, 0xc6, + 0xf8, 0x70, 0x67, 0xc6, 0xf8, 0x68, 0x67, 0xc6, 0xf8, 0xe3, 0xce, 0x8c, 0xf1, 0x93, 0x3f, 0xcd, + 0x1c, 0x7b, 0xa9, 0x3e, 0xdc, 0x7f, 0xa8, 0xfd, 0x2b, 0x00, 0x00, 0xff, 0xff, 0x53, 0x7e, 0xc9, + 0xb1, 0xd2, 0x36, 0x00, 0x00, } func (m *AddressGroup) Marshal() (dAtA []byte, err error) { @@ -2034,6 +2036,13 @@ func (m *BundleFileServer) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.HostPublicKey != nil { + i -= len(m.HostPublicKey) + copy(dAtA[i:], m.HostPublicKey) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.HostPublicKey))) + i-- + dAtA[i] = 0x12 + } i -= len(m.URL) copy(dAtA[i:], m.URL) i = encodeVarintGenerated(dAtA, i, uint64(len(m.URL))) @@ -4287,6 +4296,10 @@ func (m *BundleFileServer) Size() (n int) { _ = l l = len(m.URL) n += 1 + l + sovGenerated(uint64(l)) + if m.HostPublicKey != nil { + l = len(m.HostPublicKey) + n += 1 + l + sovGenerated(uint64(l)) + } return n } @@ -5217,6 +5230,7 @@ func (this *BundleFileServer) String() string { } s := strings.Join([]string{`&BundleFileServer{`, `URL:` + fmt.Sprintf("%v", this.URL) + `,`, + `HostPublicKey:` + valueToStringGenerated(this.HostPublicKey) + `,`, `}`, }, "") return s @@ -6814,6 +6828,40 @@ func (m *BundleFileServer) Unmarshal(dAtA []byte) error { } m.URL = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field HostPublicKey", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.HostPublicKey = append(m.HostPublicKey[:0], dAtA[iNdEx:postIndex]...) + if m.HostPublicKey == nil { + m.HostPublicKey = []byte{} + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/pkg/apis/controlplane/v1beta2/generated.proto b/pkg/apis/controlplane/v1beta2/generated.proto index 3d72cc7a63d..67f66c02f67 100644 --- a/pkg/apis/controlplane/v1beta2/generated.proto +++ b/pkg/apis/controlplane/v1beta2/generated.proto @@ -82,6 +82,8 @@ message BasicAuthentication { message BundleFileServer { optional string url = 1; + + optional bytes hostPublicKey = 2; } message BundleServerAuthConfiguration { diff --git a/pkg/apis/controlplane/v1beta2/types.go b/pkg/apis/controlplane/v1beta2/types.go index 13647845abf..96ee7ccbf28 100644 --- a/pkg/apis/controlplane/v1beta2/types.go +++ b/pkg/apis/controlplane/v1beta2/types.go @@ -587,7 +587,8 @@ type SupportBundleCollectionNodeStatus struct { } type BundleFileServer struct { - URL string `json:"url" protobuf:"bytes,1,opt,name=url"` + URL string `json:"url" protobuf:"bytes,1,opt,name=url"` + HostPublicKey []byte `json:"hostPublicKey,omitempty" protobuf:"bytes,2,opt,name=hostPublicKey"` } type BasicAuthentication struct { diff --git a/pkg/apis/controlplane/v1beta2/zz_generated.conversion.go b/pkg/apis/controlplane/v1beta2/zz_generated.conversion.go index cae3cf25d97..4ea8ca2c61f 100644 --- a/pkg/apis/controlplane/v1beta2/zz_generated.conversion.go +++ b/pkg/apis/controlplane/v1beta2/zz_generated.conversion.go @@ -697,6 +697,7 @@ func Convert_controlplane_BasicAuthentication_To_v1beta2_BasicAuthentication(in func autoConvert_v1beta2_BundleFileServer_To_controlplane_BundleFileServer(in *BundleFileServer, out *controlplane.BundleFileServer, s conversion.Scope) error { out.URL = in.URL + out.HostPublicKey = *(*[]byte)(unsafe.Pointer(&in.HostPublicKey)) return nil } @@ -707,6 +708,7 @@ func Convert_v1beta2_BundleFileServer_To_controlplane_BundleFileServer(in *Bundl func autoConvert_controlplane_BundleFileServer_To_v1beta2_BundleFileServer(in *controlplane.BundleFileServer, out *BundleFileServer, s conversion.Scope) error { out.URL = in.URL + out.HostPublicKey = *(*[]byte)(unsafe.Pointer(&in.HostPublicKey)) return nil } diff --git a/pkg/apis/controlplane/v1beta2/zz_generated.deepcopy.go b/pkg/apis/controlplane/v1beta2/zz_generated.deepcopy.go index 186610ce52b..6ccb899d513 100644 --- a/pkg/apis/controlplane/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/controlplane/v1beta2/zz_generated.deepcopy.go @@ -257,6 +257,11 @@ func (in *BasicAuthentication) DeepCopy() *BasicAuthentication { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BundleFileServer) DeepCopyInto(out *BundleFileServer) { *out = *in + if in.HostPublicKey != nil { + in, out := &in.HostPublicKey, &out.HostPublicKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } return } @@ -1327,7 +1332,7 @@ func (in *SupportBundleCollection) DeepCopyInto(out *SupportBundleCollection) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ExpiredAt.DeepCopyInto(&out.ExpiredAt) - out.FileServer = in.FileServer + in.FileServer.DeepCopyInto(&out.FileServer) in.Authentication.DeepCopyInto(&out.Authentication) return } diff --git a/pkg/apis/controlplane/zz_generated.deepcopy.go b/pkg/apis/controlplane/zz_generated.deepcopy.go index 66a8ce406d3..4aa28af5cdd 100644 --- a/pkg/apis/controlplane/zz_generated.deepcopy.go +++ b/pkg/apis/controlplane/zz_generated.deepcopy.go @@ -257,6 +257,11 @@ func (in *BasicAuthentication) DeepCopy() *BasicAuthentication { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BundleFileServer) DeepCopyInto(out *BundleFileServer) { *out = *in + if in.HostPublicKey != nil { + in, out := &in.HostPublicKey, &out.HostPublicKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } return } @@ -1327,7 +1332,7 @@ func (in *SupportBundleCollection) DeepCopyInto(out *SupportBundleCollection) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ExpiredAt.DeepCopyInto(&out.ExpiredAt) - out.FileServer = in.FileServer + in.FileServer.DeepCopyInto(&out.FileServer) in.Authentication.DeepCopyInto(&out.Authentication) return } diff --git a/pkg/apis/crd/v1alpha1/types.go b/pkg/apis/crd/v1alpha1/types.go index bde5f8f7d12..82b2b422d13 100644 --- a/pkg/apis/crd/v1alpha1/types.go +++ b/pkg/apis/crd/v1alpha1/types.go @@ -176,6 +176,10 @@ type BundleFileServer struct { // The URL of the bundle file server. It is set with format: scheme://host[:port][/path], // e.g, https://api.example.com:8443/v1/supportbundles/. If scheme is not set, https is used by default. URL string `json:"url"` + // HostPublicKey specifies the only host public key that will be accepted when connecting to + // the file server. If omitted, any host key will be accepted, which is not recommended. + // For SFTP, the key must be formatted for use in the SSH wire protocol according to RFC 4253, section 6.6. + HostPublicKey []byte `json:"hostPublicKey,omitempty"` } // BundleServerAuthType defines the authentication type to access the BundleFileServer. @@ -421,6 +425,10 @@ type PacketCaptureFileServer struct { // The URL of the file server. It is set with format: scheme://host[:port][/path], // e.g., https://api.example.com:8443/v1/packets/. Currently only `sftp` protocol is supported. URL string `json:"url"` + // HostPublicKey specifies the only host public key that will be accepted when connecting to + // the file server. If omitted, any host key will be accepted, which is not recommended. + // For SFTP, the key must be formatted for use in the SSH wire protocol according to RFC 4253, section 6.6. + HostPublicKey []byte `json:"hostPublicKey,omitempty"` } type PacketCaptureSpec struct { diff --git a/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go index d5d9153b923..4dea41e333a 100644 --- a/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/crd/v1alpha1/zz_generated.deepcopy.go @@ -207,6 +207,11 @@ func (in *BundleExternalNodes) DeepCopy() *BundleExternalNodes { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BundleFileServer) DeepCopyInto(out *BundleFileServer) { *out = *in + if in.HostPublicKey != nil { + in, out := &in.HostPublicKey, &out.HostPublicKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } return } @@ -612,6 +617,11 @@ func (in *PacketCaptureCondition) DeepCopy() *PacketCaptureCondition { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PacketCaptureFileServer) DeepCopyInto(out *PacketCaptureFileServer) { *out = *in + if in.HostPublicKey != nil { + in, out := &in.HostPublicKey, &out.HostPublicKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } return } @@ -693,7 +703,7 @@ func (in *PacketCaptureSpec) DeepCopyInto(out *PacketCaptureSpec) { if in.FileServer != nil { in, out := &in.FileServer, &out.FileServer *out = new(PacketCaptureFileServer) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -901,7 +911,7 @@ func (in *SupportBundleCollectionSpec) DeepCopyInto(out *SupportBundleCollection *out = new(BundleExternalNodes) (*in).DeepCopyInto(*out) } - out.FileServer = in.FileServer + in.FileServer.DeepCopyInto(&out.FileServer) in.Authentication.DeepCopyInto(&out.Authentication) return } diff --git a/pkg/apiserver/openapi/zz_generated.openapi.go b/pkg/apiserver/openapi/zz_generated.openapi.go index fbc976c9c2c..9b4f9f28863 100644 --- a/pkg/apiserver/openapi/zz_generated.openapi.go +++ b/pkg/apiserver/openapi/zz_generated.openapi.go @@ -813,6 +813,12 @@ func schema_pkg_apis_controlplane_v1beta2_BundleFileServer(ref common.ReferenceC Format: "", }, }, + "hostPublicKey": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "byte", + }, + }, }, Required: []string{"url"}, }, diff --git a/pkg/controller/supportbundlecollection/controller_test.go b/pkg/controller/supportbundlecollection/controller_test.go index c15048eb0a6..7c0bca6014d 100644 --- a/pkg/controller/supportbundlecollection/controller_test.go +++ b/pkg/controller/supportbundlecollection/controller_test.go @@ -45,6 +45,7 @@ import ( "antrea.io/antrea/pkg/controller/types" "antrea.io/antrea/pkg/util/auth" "antrea.io/antrea/pkg/util/k8s" + sftptesting "antrea.io/antrea/pkg/util/sftp/testing" ) const ( @@ -84,6 +85,7 @@ type bundleConfig struct { authType v1alpha1.BundleServerAuthType secretName string secretNamespace string + hostPublicKey []byte conditions []v1alpha1.SupportBundleCollectionCondition phase bundlePhase createTime *time.Time @@ -674,6 +676,9 @@ func TestCreateAndDeleteInternalSupportBundleCollection(t *testing.T) { testClient.waitForSync(stopCh) + hostPublicKey, _, err := sftptesting.GenerateEd25519Key() + require.NoError(t, err) + expiredDuration, _ := time.ParseDuration("-61m") expiredCreationTime := time.Now().Add(expiredDuration) testCases := []struct { @@ -690,7 +695,8 @@ func TestCreateAndDeleteInternalSupportBundleCollection(t *testing.T) { names: []string{"n1", "n2"}, labels: map[string]string{"test": "selected"}, }, - authType: v1alpha1.APIKey, + authType: v1alpha1.APIKey, + hostPublicKey: hostPublicKey.Marshal(), }, expectedNodes: sets.New[string]("n1", "n2", "n3", "n4"), expectedAuth: controlplane.BundleServerAuthConfiguration{ @@ -763,8 +769,9 @@ func TestCreateAndDeleteInternalSupportBundleCollection(t *testing.T) { bundleConfig.secretName = secretName bundleConfig.secretNamespace = secretNamespace } - bundle, err := testClient.crdClient.CrdV1alpha1().SupportBundleCollections().Create(context.TODO(), generateSupportBundleResource(bundleConfig), metav1.CreateOptions{}) - require.Nil(t, err) + bundle := generateSupportBundleResource(bundleConfig) + _, err := testClient.crdClient.CrdV1alpha1().SupportBundleCollections().Create(context.TODO(), bundle, metav1.CreateOptions{}) + require.NoError(t, err) err = wait.PollUntilContextTimeout(context.Background(), time.Millisecond*50, time.Second, true, func(ctx context.Context) (done bool, err error) { _, getErr := controller.supportBundleCollectionLister.Get(tc.bundleConfig.name) if getErr == nil { @@ -792,6 +799,7 @@ func TestCreateAndDeleteInternalSupportBundleCollection(t *testing.T) { internalBundle, _ := obj.(*types.SupportBundleCollection) assert.Equal(t, tc.expectedNodes, internalBundle.NodeNames) assert.Equal(t, tc.expectedAuth, internalBundle.Authentication) + assert.Equal(t, bundle.Spec.FileServer, internalBundle.FileServer) } else { updatedBundle, err := testClient.crdClient.CrdV1alpha1().SupportBundleCollections().Get(context.TODO(), bundle.Name, metav1.GetOptions{}) require.NoError(t, err) @@ -810,8 +818,7 @@ func TestCreateAndDeleteInternalSupportBundleCollection(t *testing.T) { } // Test update span - err := testClient.client.CoreV1().Nodes().Delete(context.TODO(), "n3", metav1.DeleteOptions{}) - require.NoError(t, err) + require.NoError(t, testClient.client.CoreV1().Nodes().Delete(context.TODO(), "n3", metav1.DeleteOptions{})) updatedBundleCollection := generateSupportBundleResource( bundleConfig{ name: "b1", @@ -1864,7 +1871,8 @@ func generateSupportBundleResource(b bundleConfig) *v1alpha1.SupportBundleCollec }, Spec: v1alpha1.SupportBundleCollectionSpec{ FileServer: v1alpha1.BundleFileServer{ - URL: "https://1.1.1.1:443/supportbundles/upload", + URL: "https://1.1.1.1:443/supportbundles/upload", + HostPublicKey: b.hostPublicKey, }, ExpirationMinutes: 60, SinceTime: "2h", diff --git a/pkg/controller/supportbundlecollection/store/collection.go b/pkg/controller/supportbundlecollection/store/collection.go index ae156468091..e9441b7aadf 100644 --- a/pkg/controller/supportbundlecollection/store/collection.go +++ b/pkg/controller/supportbundlecollection/store/collection.go @@ -97,7 +97,8 @@ func ToSupportBundleCollectionMsg(in *types.SupportBundleCollection, out *contro out.ExpiredAt = in.ExpiredAt out.SinceTime = in.SinceTime out.FileServer = controlplane.BundleFileServer{ - URL: in.FileServer.URL, + URL: in.FileServer.URL, + HostPublicKey: in.FileServer.HostPublicKey, } out.Authentication = in.Authentication } diff --git a/pkg/controller/supportbundlecollection/validate.go b/pkg/controller/supportbundlecollection/validate.go index 277d9aaba04..f22ba432f78 100644 --- a/pkg/controller/supportbundlecollection/validate.go +++ b/pkg/controller/supportbundlecollection/validate.go @@ -19,6 +19,7 @@ import ( "fmt" "reflect" + "golang.org/x/crypto/ssh" admv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" @@ -42,6 +43,15 @@ func (c *Controller) Validate(review *admv1.AdmissionReview) *admv1.AdmissionRes } } + validate := func(bundle *crdv1alpha1.SupportBundleCollection) error { + if bundle.Spec.FileServer.HostPublicKey != nil { + if _, err := ssh.ParsePublicKey(bundle.Spec.FileServer.HostPublicKey); err != nil { + return fmt.Errorf("invalid host public key: %w", err) + } + } + return nil + } + validateProcessingCollection := func() *admv1.AdmissionResponse { var msg string allowed := true @@ -55,8 +65,17 @@ func (c *Controller) Validate(review *admv1.AdmissionReview) *admv1.AdmissionRes return validationResult(allowed, msg) } - if review.Request.Operation == admv1.Update { + switch review.Request.Operation { + case admv1.Create: + klog.V(2).Info("Validating CREATE request for SupportBundleCollection") + if err := validate(&newObj); err != nil { + return newAdmissionResponseForErr(err) + } + case admv1.Update: klog.V(2).Info("Validating UPDATE request for SupportBundleCollection") + if err := validate(&newObj); err != nil { + return newAdmissionResponseForErr(err) + } if isCollectionCompleted(&oldObj) { return validationResult(false, fmt.Sprintf("SupportBundleCollection %s is completed, cannot be updated", oldObj.Name)) } diff --git a/pkg/controller/supportbundlecollection/validate_test.go b/pkg/controller/supportbundlecollection/validate_test.go index 4da3192bcd2..a7d5d33f3b8 100644 --- a/pkg/controller/supportbundlecollection/validate_test.go +++ b/pkg/controller/supportbundlecollection/validate_test.go @@ -20,6 +20,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" adminv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -27,11 +28,13 @@ import ( "antrea.io/antrea/pkg/apis/controlplane" crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" + sftptesting "antrea.io/antrea/pkg/util/sftp/testing" ) func TestValidateSupportBundleCollection(t *testing.T) { - bundleCollection := generateSupportBundleResource(bundleConfig{ - name: "b1", + const name = "b1" + existingConfig := &bundleConfig{ + name: name, nodes: &bundleNodes{ labels: map[string]string{"test": "selected"}, }, @@ -40,27 +43,31 @@ func TestValidateSupportBundleCollection(t *testing.T) { labels: map[string]string{"test": "selected"}, }, authType: crdv1alpha1.APIKey, - }) + } authentication := &controlplane.BundleServerAuthConfiguration{ APIKey: "bundle_api_key", } nodeSpan := sets.New[string]("n1", "n2", "n3", "n4") expiredAt := metav1.NewTime(time.Now().Add(time.Minute)) + hostPublicKey, _, err := sftptesting.GenerateEd25519Key() + require.NoError(t, err) + tests := []struct { - name string - existsInCache bool - existingStatus *crdv1alpha1.SupportBundleCollectionStatus - updatedCollection *bundleConfig - requestOperation adminv1.Operation - expectedResponse *adminv1.AdmissionResponse + name string + requestOperation adminv1.Operation + existingCollection *bundleConfig + collection *bundleConfig + existsInCache bool + existingStatus *crdv1alpha1.SupportBundleCollectionStatus + expectedResponse *adminv1.AdmissionResponse }{ { - name: "update before started", - existsInCache: false, - requestOperation: adminv1.Update, - updatedCollection: &bundleConfig{ - name: "b1", + name: "update before started", + requestOperation: adminv1.Update, + existingCollection: existingConfig, + collection: &bundleConfig{ + name: name, nodes: &bundleNodes{ labels: map[string]string{"test": "selected"}, }, @@ -71,18 +78,20 @@ func TestValidateSupportBundleCollection(t *testing.T) { }, authType: crdv1alpha1.APIKey, }, - expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, - }, { - name: "delete before started", existsInCache: false, - requestOperation: adminv1.Delete, expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, }, { - name: "update after started", - existsInCache: true, - requestOperation: adminv1.Update, - updatedCollection: &bundleConfig{ - name: "b1", + name: "delete before started", + requestOperation: adminv1.Delete, + existingCollection: existingConfig, + existsInCache: false, + expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, + }, { + name: "update after started", + requestOperation: adminv1.Update, + existingCollection: existingConfig, + collection: &bundleConfig{ + name: name, nodes: &bundleNodes{ labels: map[string]string{"test": "selected"}, }, @@ -93,6 +102,7 @@ func TestValidateSupportBundleCollection(t *testing.T) { }, authType: crdv1alpha1.APIKey, }, + existsInCache: true, expectedResponse: &adminv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ @@ -100,16 +110,11 @@ func TestValidateSupportBundleCollection(t *testing.T) { }, }, }, { - name: "update status after started", - existsInCache: true, - requestOperation: adminv1.Update, - existingStatus: &crdv1alpha1.SupportBundleCollectionStatus{ - Conditions: []crdv1alpha1.SupportBundleCollectionCondition{ - {Type: crdv1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, - }, - }, - updatedCollection: &bundleConfig{ - name: "b1", + name: "update status after started", + requestOperation: adminv1.Update, + existingCollection: existingConfig, + collection: &bundleConfig{ + name: name, nodes: &bundleNodes{ labels: map[string]string{"test": "selected"}, }, @@ -123,19 +128,19 @@ func TestValidateSupportBundleCollection(t *testing.T) { {Status: metav1.ConditionTrue, Type: crdv1alpha1.BundleCollected}, }, }, - expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, - }, { - name: "update after completed", - existsInCache: true, - requestOperation: adminv1.Update, + existsInCache: true, existingStatus: &crdv1alpha1.SupportBundleCollectionStatus{ Conditions: []crdv1alpha1.SupportBundleCollectionCondition{ {Type: crdv1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, - {Type: crdv1alpha1.CollectionCompleted, Status: metav1.ConditionTrue}, }, }, - updatedCollection: &bundleConfig{ - name: "b1", + expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, + }, { + name: "update after completed", + requestOperation: adminv1.Update, + existingCollection: existingConfig, + collection: &bundleConfig{ + name: name, nodes: &bundleNodes{ labels: map[string]string{"test": "selected"}, }, @@ -146,6 +151,13 @@ func TestValidateSupportBundleCollection(t *testing.T) { }, authType: crdv1alpha1.APIKey, }, + existsInCache: true, + existingStatus: &crdv1alpha1.SupportBundleCollectionStatus{ + Conditions: []crdv1alpha1.SupportBundleCollectionCondition{ + {Type: crdv1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + {Type: crdv1alpha1.CollectionCompleted, Status: metav1.ConditionTrue}, + }, + }, expectedResponse: &adminv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ @@ -153,17 +165,11 @@ func TestValidateSupportBundleCollection(t *testing.T) { }, }, }, { - name: "update status after completed", - existsInCache: true, - requestOperation: adminv1.Update, - existingStatus: &crdv1alpha1.SupportBundleCollectionStatus{ - Conditions: []crdv1alpha1.SupportBundleCollectionCondition{ - {Type: crdv1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, - {Type: crdv1alpha1.CollectionCompleted, Status: metav1.ConditionTrue}, - }, - }, - updatedCollection: &bundleConfig{ - name: "b1", + name: "update status after completed", + requestOperation: adminv1.Update, + existingCollection: existingConfig, + collection: &bundleConfig{ + name: name, nodes: &bundleNodes{ labels: map[string]string{"test": "selected"}, }, @@ -177,6 +183,13 @@ func TestValidateSupportBundleCollection(t *testing.T) { {Status: metav1.ConditionTrue, Type: crdv1alpha1.BundleCollected}, }, }, + existsInCache: true, + existingStatus: &crdv1alpha1.SupportBundleCollectionStatus{ + Conditions: []crdv1alpha1.SupportBundleCollectionCondition{ + {Type: crdv1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + {Type: crdv1alpha1.CollectionCompleted, Status: metav1.ConditionTrue}, + }, + }, expectedResponse: &adminv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ @@ -184,19 +197,21 @@ func TestValidateSupportBundleCollection(t *testing.T) { }, }, }, { - name: "delete after started", - existsInCache: true, + name: "delete after started", + requestOperation: adminv1.Delete, + existingCollection: existingConfig, + existsInCache: true, existingStatus: &crdv1alpha1.SupportBundleCollectionStatus{ Conditions: []crdv1alpha1.SupportBundleCollectionCondition{ {Type: crdv1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, }, }, - requestOperation: adminv1.Delete, expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, }, { - name: "delete after completed", - existsInCache: true, - requestOperation: adminv1.Delete, + name: "delete after completed", + requestOperation: adminv1.Delete, + existingCollection: existingConfig, + existsInCache: true, existingStatus: &crdv1alpha1.SupportBundleCollectionStatus{ Conditions: []crdv1alpha1.SupportBundleCollectionCondition{ {Type: crdv1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, @@ -204,6 +219,30 @@ func TestValidateSupportBundleCollection(t *testing.T) { }, }, expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, + }, { + name: "create with host public key", + requestOperation: adminv1.Create, + collection: &bundleConfig{ + name: name, + authType: crdv1alpha1.APIKey, + hostPublicKey: hostPublicKey.Marshal(), + }, + expectedResponse: &adminv1.AdmissionResponse{Allowed: true}, + }, { + name: "create with invalid host public key", + requestOperation: adminv1.Create, + collection: &bundleConfig{ + name: name, + authType: crdv1alpha1.APIKey, + // invalid key + hostPublicKey: []byte("abc"), + }, + expectedResponse: &adminv1.AdmissionResponse{ + Allowed: false, + Result: &metav1.Status{ + Message: "invalid host public key: ssh: short read", + }, + }, }, } for _, tt := range tests { @@ -214,23 +253,25 @@ func TestValidateSupportBundleCollection(t *testing.T) { controller := newController(testClient) testClient.start(stopCh) testClient.waitForSync(stopCh) + var bundleCollection, existingBundleCollection *crdv1alpha1.SupportBundleCollection + if tt.existingCollection != nil { + existingBundleCollection = generateSupportBundleResource(*tt.existingCollection) + } + if tt.collection != nil { + bundleCollection = generateSupportBundleResource(*tt.collection) + } if tt.existsInCache { - controller.addInternalSupportBundleCollection(bundleCollection, nodeSpan, authentication, expiredAt) + controller.addInternalSupportBundleCollection(existingBundleCollection, nodeSpan, authentication, expiredAt) } - oldBundleCollection := bundleCollection if tt.existingStatus != nil { - oldBundleCollection.Status = *tt.existingStatus - } - newBundleCollection := oldBundleCollection - if tt.updatedCollection != nil { - newBundleCollection = generateSupportBundleResource(*tt.updatedCollection) + existingBundleCollection.Status = *tt.existingStatus } review := &adminv1.AdmissionReview{ Request: &adminv1.AdmissionRequest{ - Name: bundleCollection.Name, + Name: name, Operation: tt.requestOperation, - OldObject: runtime.RawExtension{Raw: marshal(oldBundleCollection)}, - Object: runtime.RawExtension{Raw: marshal(newBundleCollection)}, + OldObject: runtime.RawExtension{Raw: marshal(existingBundleCollection)}, + Object: runtime.RawExtension{Raw: marshal(bundleCollection)}, }, } gotResponse := controller.Validate(review) @@ -240,6 +281,9 @@ func TestValidateSupportBundleCollection(t *testing.T) { } func marshal(object runtime.Object) []byte { + if object == nil { + return nil + } raw, _ := json.Marshal(object) return raw } diff --git a/pkg/util/sftp/ssh.go b/pkg/util/sftp/ssh.go new file mode 100644 index 00000000000..de6c4731466 --- /dev/null +++ b/pkg/util/sftp/ssh.go @@ -0,0 +1,62 @@ +// Copyright 2024 Antrea 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. + +package sftp + +import ( + "fmt" + "time" + + "golang.org/x/crypto/ssh" +) + +// getAlgorithmsForHostKey returns the list of supported key algorithms for a given public key. In +// most cases, there is a single algorithm, which matches the key type. This is useful when setting +// the HostKeyCallback in ssh.ClientConfig to accept a fixed host key. The server may support +// multiple key types / key algorithms, and if we use the default value for HostKeyAlgorithms, the +// server may present a key that does not match our HostKeyCallback. When using a fixed host key, it +// makes sense to set HostKeyAlgorithms to the list of algorithms matching that specific key type. +func getAlgorithmsForHostKey(key ssh.PublicKey) []string { + switch t := key.Type(); t { + case ssh.KeyAlgoRSA: + return []string{ssh.KeyAlgoRSA, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512} + default: + return []string{t} + } +} + +// GetSSHClientConfig returns a standard SSH client configuration which is used by Antrea for SFTP +// upload. If hostKey is nil (not recommended), the config will accept any host public key. +func GetSSHClientConfig(user string, password string, hostKey []byte) (*ssh.ClientConfig, error) { + // #nosec G106: users should provie hostKey, accepting arbitrary keys is not recommended. + hostKeyCallback := ssh.InsecureIgnoreHostKey() + var hostKeyAlgorithms []string + if hostKey != nil { + key, err := ssh.ParsePublicKey(hostKey) + if err != nil { + return nil, fmt.Errorf("invalid host public key: %w", err) + } + hostKeyCallback = ssh.FixedHostKey(key) + // With a single fixed key, it makes sense to set this in case the server supports + // multiple keys (e.g., ed25519 and rsa). + hostKeyAlgorithms = getAlgorithmsForHostKey(key) + } + return &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ssh.Password(password)}, + HostKeyCallback: hostKeyCallback, + HostKeyAlgorithms: hostKeyAlgorithms, + Timeout: 1 * time.Second, + }, nil +} diff --git a/pkg/util/sftp/ssh_test.go b/pkg/util/sftp/ssh_test.go new file mode 100644 index 00000000000..07b7d65f965 --- /dev/null +++ b/pkg/util/sftp/ssh_test.go @@ -0,0 +1,88 @@ +// Copyright 2024 Antrea 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. + +package sftp + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" + + sftptesting "antrea.io/antrea/pkg/util/sftp/testing" +) + +func TestGetSSHClientConfig(t *testing.T) { + const ( + user = "foo" + password = "bar" + ) + rsaPubKey, _, err := sftptesting.GenerateRSAKey(4096) + require.NoError(t, err) + ed25519PubKey, _, err := sftptesting.GenerateEd25519Key() + require.NoError(t, err) + + testCases := []struct { + name string + hostKey []byte + expectedErr string + expectedHostKeyAlgorithms []string + rsaKeyValid bool + ed25519KeyValid bool + }{ + { + name: "invalid key format", + hostKey: []byte("abc"), + expectedErr: "invalid host public key", + }, + { + name: "ignore host key", + hostKey: nil, + rsaKeyValid: true, + ed25519KeyValid: true, + }, + { + name: "rsa key only", + hostKey: rsaPubKey.Marshal(), + expectedHostKeyAlgorithms: []string{ssh.KeyAlgoRSA, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512}, + rsaKeyValid: true, + ed25519KeyValid: false, + }, + { + name: "ed25519 key only", + hostKey: ed25519PubKey.Marshal(), + expectedHostKeyAlgorithms: []string{ssh.KeyAlgoED25519}, + rsaKeyValid: false, + ed25519KeyValid: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cfg, err := GetSSHClientConfig(user, password, tc.hostKey) + if tc.expectedErr != "" { + assert.ErrorContains(t, err, tc.expectedErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedHostKeyAlgorithms, cfg.HostKeyAlgorithms) + require.NotNil(t, cfg.HostKeyCallback) + rsaKeyValid := cfg.HostKeyCallback("", nil, rsaPubKey) == nil + assert.Equal(t, tc.rsaKeyValid, rsaKeyValid, "Invalid HostKeyCallback result for RSA key") + ed25519KeyValid := cfg.HostKeyCallback("", nil, ed25519PubKey) == nil + assert.Equal(t, tc.ed25519KeyValid, ed25519KeyValid, "Invalid HostKeyCallback result for Ed25519 key") + }) + } +} diff --git a/pkg/util/sftp/testing/ssh.go b/pkg/util/sftp/testing/ssh.go new file mode 100644 index 00000000000..ed00cb937dc --- /dev/null +++ b/pkg/util/sftp/testing/ssh.go @@ -0,0 +1,61 @@ +// Copyright 2024 Antrea 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. + +package testing + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "encoding/pem" + + "golang.org/x/crypto/ssh" +) + +// GenerateRSAKey generates a RSA key pair. The public key is returned as a ssh.PublicKey. The +// private key is returned as PEM data, serialized in the OpenSSH format. +func GenerateRSAKey(bits int) (ssh.PublicKey, []byte, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + publicKeySSH, err := ssh.NewPublicKey(&privateKey.PublicKey) + if err != nil { + return nil, nil, err + } + privateKeyPEM, err := ssh.MarshalPrivateKey(crypto.PrivateKey(privateKey), "") + if err != nil { + return nil, nil, err + } + return publicKeySSH, pem.EncodeToMemory(privateKeyPEM), nil +} + +// GenerateRSAKey generates a ed25519 key pair. The public key is returned as a ssh.PublicKey. The +// private key is returned as PEM data, serialized in the OpenSSH format. +func GenerateEd25519Key() (ssh.PublicKey, []byte, error) { + publicKey, privateKey, err := ed25519.GenerateKey(nil) + if err != nil { + return nil, nil, err + } + publicKeySSH, err := ssh.NewPublicKey(publicKey) + if err != nil { + return nil, nil, err + } + privateKeyPEM, err := ssh.MarshalPrivateKey(crypto.PrivateKey(privateKey), "") + if err != nil { + return nil, nil, err + } + return publicKeySSH, pem.EncodeToMemory(privateKeyPEM), nil +} diff --git a/test/e2e/framework.go b/test/e2e/framework.go index 38901711ff6..387445e5adc 100644 --- a/test/e2e/framework.go +++ b/test/e2e/framework.go @@ -2840,7 +2840,7 @@ func (data *TestData) collectCovFiles(podName string, containerName string, nsNa continue } if err := wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, 5*time.Second, true, func(ctx context.Context) (bool, error) { - if err = data.copyPodFiles(podName, containerName, nsName, file, covDir); err != nil { + if err = data.copyPodFile(podName, containerName, nsName, file, covDir); err != nil { log.Infof("Coverage file not available yet for copy: %v", err) return false, nil } @@ -2880,11 +2880,19 @@ func (data *TestData) collectAntctlCovFilesFromControlPlaneNode(covDir string) e } -// copyPodFiles copies file from a Pod and save it to specified directory -func (data *TestData) copyPodFiles(podName string, containerName string, nsName string, fileName string, destDir string) error { - // getPodWriter creates the file with name podName-fileName-suffix. It returns nil if the - // file cannot be created. File must be closed by the caller. - getPodWriter := func(fileName string) *os.File { +// readPodFile reads a file from a Pod and returns the file contents as a string. +func (data *TestData) readPodFile(podName string, containerName string, nsName string, fileName string) (string, error) { + cmd := []string{"cat", fileName} + stdout, stderr, err := data.RunCommandFromPod(nsName, podName, containerName, cmd) + if err != nil { + return "", fmt.Errorf("cannot retrieve content of file '%s' from Pod '%s', stderr: <%v>, err: <%v>", fileName, podName, stderr, err) + } + return stdout, nil +} + +// copyPodFile copies a file from a Pod and save it to specified directory. +func (data *TestData) copyPodFile(podName string, containerName string, nsName string, fileName string, destDir string) error { + getWriter := func(fileName string) *os.File { destFile := filepath.Join(destDir, fileName) f, err := os.Create(destFile) if err != nil { @@ -2895,20 +2903,16 @@ func (data *TestData) copyPodFiles(podName string, containerName string, nsName } // dump the file from Antrea Pods to disk. basename := path.Base(fileName) - w := getPodWriter(basename) + w := getWriter(basename) if w == nil { return nil } defer w.Close() - cmd := []string{"cat", fileName} - log.Infof("Copying file: %s", basename) - stdout, stderr, err := data.RunCommandFromPod(nsName, podName, containerName, cmd) + stdout, err := data.readPodFile(podName, containerName, nsName, fileName) if err != nil { - return fmt.Errorf("cannot retrieve content of file '%s' from Pod '%s', stderr: <%v>, err: <%v>", fileName, podName, stderr, err) - } - if stdout == "" { - return nil + return err } + log.Infof("Copying file %q from Pod %s/%s", fileName, nsName, podName) w.WriteString(stdout) return nil } diff --git a/test/e2e/packetcapture_test.go b/test/e2e/packetcapture_test.go index aa728e1c5f2..28e1f41f1a1 100644 --- a/test/e2e/packetcapture_test.go +++ b/test/e2e/packetcapture_test.go @@ -42,6 +42,7 @@ import ( crdv1alpha1 "antrea.io/antrea/pkg/apis/crd/v1alpha1" "antrea.io/antrea/pkg/features" + sftptesting "antrea.io/antrea/pkg/util/sftp/testing" ) var ( @@ -80,6 +81,19 @@ func genSFTPService() *v1.Service { } } +func genSSHKeysSecret(ed25519Key, rsaKey []byte) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "ssh-keys", + }, + Immutable: ptr.To(true), + Data: map[string][]byte{ + "ed25519": ed25519Key, + "rsa": rsaKey, + }, + } +} + func genSFTPDeployment() *appsv1.Deployment { replicas := int32(1) selector := map[string]string{"app": "sftp"} @@ -113,6 +127,31 @@ func genSFTPDeployment() *appsv1.Deployment { }, PeriodSeconds: 3, }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "ssh-keys", + ReadOnly: true, + MountPath: "/etc/ssh/ssh_host_ed25519_key", + SubPath: "ed25519", + }, + { + Name: "ssh-keys", + ReadOnly: true, + MountPath: "/etc/ssh/ssh_host_rsa_key", + SubPath: "rsa", + }, + }, + }, + }, + Volumes: []v1.Volume{ + { + Name: "ssh-keys", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: "ssh-keys", + DefaultMode: ptr.To[int32](0400), + }, + }, }, }, }, @@ -142,13 +181,18 @@ func TestPacketCapture(t *testing.T) { } defer teardownTest(t, data) + ed25519PubKey, ed25519PrivateKey, err := sftptesting.GenerateEd25519Key() + require.NoError(t, err) + rsaPubKey, rsaPrivateKey, err := sftptesting.GenerateRSAKey(4096) + require.NoError(t, err) + + _, err = data.clientset.CoreV1().Secrets(data.testNamespace).Create(context.TODO(), genSSHKeysSecret(ed25519PrivateKey, rsaPrivateKey), metav1.CreateOptions{}) + require.NoError(t, err) deployment, err := data.clientset.AppsV1().Deployments(data.testNamespace).Create(context.TODO(), genSFTPDeployment(), metav1.CreateOptions{}) require.NoError(t, err) - defer data.clientset.AppsV1().Deployments(data.testNamespace).Delete(context.TODO(), deployment.Name, metav1.DeleteOptions{}) - svc, err := data.clientset.CoreV1().Services(data.testNamespace).Create(context.TODO(), genSFTPService(), metav1.CreateOptions{}) + _, err = data.clientset.CoreV1().Services(data.testNamespace).Create(context.TODO(), genSFTPService(), metav1.CreateOptions{}) require.NoError(t, err) - defer data.clientset.CoreV1().Services(data.testNamespace).Delete(context.TODO(), svc.Name, metav1.DeleteOptions{}) - failOnError(data.waitForDeploymentReady(t, data.testNamespace, "sftp", defaultTimeout), t) + failOnError(data.waitForDeploymentReady(t, deployment.Namespace, deployment.Name, defaultTimeout), t) sec := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -166,14 +210,14 @@ func TestPacketCapture(t *testing.T) { defer data.clientset.CoreV1().Secrets(sec.Namespace).Delete(context.TODO(), sec.Name, metav1.DeleteOptions{}) t.Run("testPacketCaptureBasic", func(t *testing.T) { - testPacketCaptureBasic(t, data) + testPacketCaptureBasic(t, data, ed25519PubKey.Marshal(), rsaPubKey.Marshal()) }) } // testPacketCaptureTCP verifies if PacketCapture can capture tcp packets. this function only contains basic // cases with pod-to-pod. -func testPacketCaptureBasic(t *testing.T, data *TestData) { +func testPacketCaptureBasic(t *testing.T, data *TestData, ed25519PubKey, rsaPubKey []byte) { node1 := nodeName(0) clientPodName := "client" tcpServerPodName := "tcp-server" @@ -329,7 +373,8 @@ func testPacketCaptureBasic(t *testing.T, data *TestData) { }, }, FileServer: &crdv1alpha1.PacketCaptureFileServer{ - URL: fmt.Sprintf("sftp://%s:30010/upload", controlPlaneNodeIPv4()), + URL: fmt.Sprintf("sftp://%s:30010/upload", controlPlaneNodeIPv4()), + HostPublicKey: ed25519PubKey, }, Packet: &crdv1alpha1.Packet{ Protocol: &tcpProto, @@ -393,7 +438,8 @@ func testPacketCaptureBasic(t *testing.T, data *TestData) { }, }, FileServer: &crdv1alpha1.PacketCaptureFileServer{ - URL: fmt.Sprintf("sftp://%s:30010/upload", controlPlaneNodeIPv4()), + URL: fmt.Sprintf("sftp://%s:30010/upload", controlPlaneNodeIPv4()), + HostPublicKey: rsaPubKey, }, Packet: &crdv1alpha1.Packet{ Protocol: &udpProto, @@ -617,7 +663,7 @@ func runPacketCaptureTest(t *testing.T, data *TestData, tc pcTestCase) { tmpDir := t.TempDir() dstFileName := filepath.Join(tmpDir, fileName) packetFile := filepath.Join("/tmp", "antrea", "packetcapture", "packets", fileName) - require.NoError(t, data.copyPodFiles(antreaPodName, "antrea-agent", "kube-system", packetFile, tmpDir)) + require.NoError(t, data.copyPodFile(antreaPodName, "antrea-agent", "kube-system", packetFile, tmpDir)) defer os.Remove(dstFileName) file, err := os.Open(dstFileName) require.NoError(t, err)