diff --git a/config/crds/troubleshoot.sh_analyzers.yaml b/config/crds/troubleshoot.sh_analyzers.yaml index 60517b8e9..9151c9954 100644 --- a/config/crds/troubleshoot.sh_analyzers.yaml +++ b/config/crds/troubleshoot.sh_analyzers.yaml @@ -2557,6 +2557,55 @@ spec: required: - outcomes type: object + networkNamespaceConnectivity: + properties: + annotations: + additionalProperties: + type: string + type: object + checkName: + type: string + collectorName: + type: string + exclude: + type: BoolString + outcomes: + items: + properties: + fail: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + pass: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + warn: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + type: object + type: array + strict: + type: BoolString + required: + - outcomes + type: object subnetAvailable: properties: annotations: diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml index ed5222823..ba955f220 100644 --- a/config/crds/troubleshoot.sh_collectors.yaml +++ b/config/crds/troubleshoot.sh_collectors.yaml @@ -17326,6 +17326,25 @@ spec: exclude: type: BoolString type: object + networkNamespaceConnectivity: + properties: + collectorName: + type: string + exclude: + type: BoolString + fromCIDR: + type: string + port: + type: integer + timeout: + type: string + toCIDR: + type: string + required: + - fromCIDR + - port + - toCIDR + type: object run: properties: args: diff --git a/config/crds/troubleshoot.sh_hostcollectors.yaml b/config/crds/troubleshoot.sh_hostcollectors.yaml index 14414a0b3..701301bc7 100644 --- a/config/crds/troubleshoot.sh_hostcollectors.yaml +++ b/config/crds/troubleshoot.sh_hostcollectors.yaml @@ -797,6 +797,55 @@ spec: required: - outcomes type: object + networkNamespaceConnectivity: + properties: + annotations: + additionalProperties: + type: string + type: object + checkName: + type: string + collectorName: + type: string + exclude: + type: BoolString + outcomes: + items: + properties: + fail: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + pass: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + warn: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + type: object + type: array + strict: + type: BoolString + required: + - outcomes + type: object subnetAvailable: properties: annotations: @@ -1603,6 +1652,25 @@ spec: exclude: type: BoolString type: object + networkNamespaceConnectivity: + properties: + collectorName: + type: string + exclude: + type: BoolString + fromCIDR: + type: string + port: + type: integer + timeout: + type: string + toCIDR: + type: string + required: + - fromCIDR + - port + - toCIDR + type: object run: properties: args: diff --git a/config/crds/troubleshoot.sh_hostpreflights.yaml b/config/crds/troubleshoot.sh_hostpreflights.yaml index 1b1bf828b..236862169 100644 --- a/config/crds/troubleshoot.sh_hostpreflights.yaml +++ b/config/crds/troubleshoot.sh_hostpreflights.yaml @@ -797,6 +797,55 @@ spec: required: - outcomes type: object + networkNamespaceConnectivity: + properties: + annotations: + additionalProperties: + type: string + type: object + checkName: + type: string + collectorName: + type: string + exclude: + type: BoolString + outcomes: + items: + properties: + fail: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + pass: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + warn: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + type: object + type: array + strict: + type: BoolString + required: + - outcomes + type: object subnetAvailable: properties: annotations: @@ -1603,6 +1652,25 @@ spec: exclude: type: BoolString type: object + networkNamespaceConnectivity: + properties: + collectorName: + type: string + exclude: + type: BoolString + fromCIDR: + type: string + port: + type: integer + timeout: + type: string + toCIDR: + type: string + required: + - fromCIDR + - port + - toCIDR + type: object run: properties: args: diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index 2c8c5c2ed..7430d68f3 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -19444,6 +19444,55 @@ spec: required: - outcomes type: object + networkNamespaceConnectivity: + properties: + annotations: + additionalProperties: + type: string + type: object + checkName: + type: string + collectorName: + type: string + exclude: + type: BoolString + outcomes: + items: + properties: + fail: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + pass: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + warn: + properties: + message: + type: string + uri: + type: string + when: + type: string + type: object + type: object + type: array + strict: + type: BoolString + required: + - outcomes + type: object subnetAvailable: properties: annotations: @@ -20250,6 +20299,25 @@ spec: exclude: type: BoolString type: object + networkNamespaceConnectivity: + properties: + collectorName: + type: string + exclude: + type: BoolString + fromCIDR: + type: string + port: + type: integer + timeout: + type: string + toCIDR: + type: string + required: + - fromCIDR + - port + - toCIDR + type: object run: properties: args: diff --git a/go.mod b/go.mod index 077e6da1c..d6814fc30 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,8 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 github.com/tj/go-spin v1.1.0 + github.com/vishvananda/netlink v1.2.1-beta.2 + github.com/vishvananda/netns v0.0.4 github.com/vmware-tanzu/velero v1.14.1 go.opentelemetry.io/otel v1.31.0 go.opentelemetry.io/otel/sdk v1.31.0 @@ -113,6 +115,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/sylabs/sif/v2 v2.18.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/vladimirvivien/gexe v0.3.0 // indirect diff --git a/go.sum b/go.sum index 45488eddd..0dc63c576 100644 --- a/go.sum +++ b/go.sum @@ -885,6 +885,11 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vladimirvivien/gexe v0.3.0 h1:4xwiOwGrDob5OMR6E92B9olDXYDglXdHhzR1ggYtWJM= github.com/vladimirvivien/gexe v0.3.0/go.mod h1:fp7cy60ON1xjhtEI/+bfSEIXX35qgmI+iRYlGOqbBFM= github.com/vmware-tanzu/velero v1.14.1 h1:HYj73scn7ZqtfTanjW/X4W0Hn3w/qcfoRbrHCWM52iI= @@ -1131,6 +1136,7 @@ golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1139,6 +1145,7 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/analyze/host_analyzer.go b/pkg/analyze/host_analyzer.go index 5f25f312c..47caa3797 100644 --- a/pkg/analyze/host_analyzer.go +++ b/pkg/analyze/host_analyzer.go @@ -61,6 +61,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b return &AnalyzeHostKernelConfigs{analyzer.KernelConfigs}, true case analyzer.JsonCompare != nil: return &AnalyzeHostJsonCompare{analyzer.JsonCompare}, true + case analyzer.NetworkNamespaceConnectivity != nil: + return &AnalyzeHostNetworkNamespaceConnectivity{analyzer.NetworkNamespaceConnectivity}, true default: return nil, false } diff --git a/pkg/analyze/host_network_namespace_connectivity.go b/pkg/analyze/host_network_namespace_connectivity.go new file mode 100644 index 000000000..62ecfb723 --- /dev/null +++ b/pkg/analyze/host_network_namespace_connectivity.go @@ -0,0 +1,80 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "path/filepath" + + util "github.com/replicatedhq/troubleshoot/internal/util" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" +) + +type AnalyzeHostNetworkNamespaceConnectivity struct { + hostAnalyzer *troubleshootv1beta2.NetworkNamespaceConnectivityAnalyze +} + +func (a *AnalyzeHostNetworkNamespaceConnectivity) Title() string { + return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Network Namespace Connectivity") +} + +func (a *AnalyzeHostNetworkNamespaceConnectivity) IsExcluded() (bool, error) { + return isExcluded(a.hostAnalyzer.Exclude) +} + +func (a *AnalyzeHostNetworkNamespaceConnectivity) Analyze( + getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents, +) ([]*AnalyzeResult, error) { + hostAnalyzer := a.hostAnalyzer + + collectedPath := filepath.Join("host-collectors/system", "networkNamespaceConnectivity.json") + fileName := "networkNamespaceConnectivity.json" + if hostAnalyzer.CollectorName != "" { + collectedPath = filepath.Join("host-collectors/system", hostAnalyzer.CollectorName+".json") + fileName = hostAnalyzer.CollectorName + ".json" + } + + collectedContents, err := retrieveCollectedContents( + getCollectedFileContents, + collectedPath, + collect.NodeInfoBaseDir, + fileName, + ) + if err != nil { + return nil, fmt.Errorf("failed to retrieve collected contents: %w", err) + } + + var results []*AnalyzeResult + for _, collected := range collectedContents { + var info collect.NetworkNamespaceConnectivityInfo + if err := json.Unmarshal(collected.Data, &info); err != nil { + return nil, fmt.Errorf("failed to unmarshal disk usage info: %w", err) + } + + for _, outcome := range hostAnalyzer.Outcomes { + result := &AnalyzeResult{Title: a.Title()} + + if outcome.Pass != nil && info.Success { + result.IsPass = true + result.Message, err = util.RenderTemplate(outcome.Pass.Message, &info) + if err != nil { + return nil, fmt.Errorf("failed to render template on outcome message: %w", err) + } + results = append(results, result) + break + } + + if outcome.Fail != nil && !info.Success { + result.IsFail = true + result.Message, err = util.RenderTemplate(outcome.Fail.Message, &info) + if err != nil { + return nil, fmt.Errorf("failed to render template on outcome message: %w", err) + } + results = append(results, result) + break + } + } + } + + return results, nil +} diff --git a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go index e9e6fa26f..3d5c92984 100644 --- a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go @@ -6,6 +6,12 @@ type CPUAnalyze struct { Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` } +type NetworkNamespaceConnectivityAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + type MemoryAnalyze struct { AnalyzeMeta `json:",inline" yaml:",inline"` CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` @@ -130,27 +136,28 @@ type KernelConfigsAnalyze struct { } type HostAnalyze struct { - CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"` - TCPLoadBalancer *TCPLoadBalancerAnalyze `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"` - HTTPLoadBalancer *HTTPLoadBalancerAnalyze `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"` - DiskUsage *DiskUsageAnalyze `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"` - Memory *MemoryAnalyze `json:"memory,omitempty" yaml:"memory,omitempty"` - TCPPortStatus *TCPPortStatusAnalyze `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"` - UDPPortStatus *UDPPortStatusAnalyze `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"` - HTTP *HTTPAnalyze `json:"http,omitempty" yaml:"http,omitempty"` - Time *TimeAnalyze `json:"time,omitempty" yaml:"time,omitempty"` - BlockDevices *BlockDevicesAnalyze `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"` - SystemPackages *SystemPackagesAnalyze `json:"systemPackages,omitempty" yaml:"systemPackages,omitempty"` - KernelModules *KernelModulesAnalyze `json:"kernelModules,omitempty" yaml:"kernelModules,omitempty"` - TCPConnect *TCPConnectAnalyze `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"` - IPV4Interfaces *IPV4InterfacesAnalyze `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"` - SubnetAvailable *SubnetAvailableAnalyze `json:"subnetAvailable,omitempty" yaml:"subnetAvailable,omitempty"` - FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"` - Certificate *CertificateAnalyze `json:"certificate,omitempty" yaml:"certificate,omitempty"` - CertificatesCollection *HostCertificatesCollectionAnalyze `json:"certificatesCollection,omitempty" yaml:"certificatesCollection,omitempty"` - HostServices *HostServicesAnalyze `json:"hostServices,omitempty" yaml:"hostServices,omitempty"` - HostOS *HostOSAnalyze `json:"hostOS,omitempty" yaml:"hostOS,omitempty"` - TextAnalyze *TextAnalyze `json:"textAnalyze,omitempty" yaml:"textAnalyze,omitempty"` - KernelConfigs *KernelConfigsAnalyze `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"` - JsonCompare *JsonCompare `json:"jsonCompare,omitempty" yaml:"jsonCompare,omitempty"` + CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"` + TCPLoadBalancer *TCPLoadBalancerAnalyze `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"` + HTTPLoadBalancer *HTTPLoadBalancerAnalyze `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"` + DiskUsage *DiskUsageAnalyze `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"` + Memory *MemoryAnalyze `json:"memory,omitempty" yaml:"memory,omitempty"` + TCPPortStatus *TCPPortStatusAnalyze `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"` + UDPPortStatus *UDPPortStatusAnalyze `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"` + HTTP *HTTPAnalyze `json:"http,omitempty" yaml:"http,omitempty"` + Time *TimeAnalyze `json:"time,omitempty" yaml:"time,omitempty"` + BlockDevices *BlockDevicesAnalyze `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"` + SystemPackages *SystemPackagesAnalyze `json:"systemPackages,omitempty" yaml:"systemPackages,omitempty"` + KernelModules *KernelModulesAnalyze `json:"kernelModules,omitempty" yaml:"kernelModules,omitempty"` + TCPConnect *TCPConnectAnalyze `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"` + IPV4Interfaces *IPV4InterfacesAnalyze `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"` + SubnetAvailable *SubnetAvailableAnalyze `json:"subnetAvailable,omitempty" yaml:"subnetAvailable,omitempty"` + FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"` + Certificate *CertificateAnalyze `json:"certificate,omitempty" yaml:"certificate,omitempty"` + CertificatesCollection *HostCertificatesCollectionAnalyze `json:"certificatesCollection,omitempty" yaml:"certificatesCollection,omitempty"` + HostServices *HostServicesAnalyze `json:"hostServices,omitempty" yaml:"hostServices,omitempty"` + HostOS *HostOSAnalyze `json:"hostOS,omitempty" yaml:"hostOS,omitempty"` + TextAnalyze *TextAnalyze `json:"textAnalyze,omitempty" yaml:"textAnalyze,omitempty"` + KernelConfigs *KernelConfigsAnalyze `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"` + JsonCompare *JsonCompare `json:"jsonCompare,omitempty" yaml:"jsonCompare,omitempty"` + NetworkNamespaceConnectivity *NetworkNamespaceConnectivityAnalyze `json:"networkNamespaceConnectivity,omitempty" yaml:"networkNamespaceConnectivity,omitempty"` } diff --git a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go index 9d17ee47a..6cdbb7bc7 100644 --- a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go @@ -76,6 +76,14 @@ type HostCopy struct { Path string `json:"path" yaml:"path"` } +type HostNetworkNamespaceConnectivity struct { + HostCollectorMeta `json:",inline" yaml:",inline"` + FromCIDR string `json:"fromCIDR" yaml:"fromCIDR"` + ToCIDR string `json:"toCIDR" yaml:"toCIDR"` + Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Port int `json:"port" yaml:"port"` +} + type HostCGroups struct { HostCollectorMeta `json:",inline" yaml:",inline"` MountPoint string `json:"mountPoint,omitempty" yaml:"mountPoint,omitempty"` @@ -224,33 +232,34 @@ type HostDNS struct { } type HostCollect struct { - CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"` - Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"` - TCPLoadBalancer *TCPLoadBalancer `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"` - HTTPLoadBalancer *HTTPLoadBalancer `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"` - TCPPortStatus *TCPPortStatus `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"` - UDPPortStatus *UDPPortStatus `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"` - Kubernetes *Kubernetes `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"` - IPV4Interfaces *IPV4Interfaces `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"` - SubnetAvailable *SubnetAvailable `json:"subnetAvailable,omitempty" yaml:"subnetAvailable,omitempty"` - DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"` - HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"` - Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"` - BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"` - SystemPackages *HostSystemPackages `json:"systemPackages,omitempty" yaml:"systemPackages,omitempty"` - KernelModules *HostKernelModules `json:"kernelModules,omitempty" yaml:"kernelModules,omitempty"` - TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"` - FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"` - Certificate *Certificate `json:"certificate,omitempty" yaml:"certificate,omitempty"` - CertificatesCollection *HostCertificatesCollection `json:"certificatesCollection,omitempty" yaml:"certificatesCollection,omitempty"` - HostServices *HostServices `json:"hostServices,omitempty" yaml:"hostServices,omitempty"` - HostOS *HostOS `json:"hostOS,omitempty" yaml:"hostOS,omitempty"` - HostRun *HostRun `json:"run,omitempty" yaml:"run,omitempty"` - HostCopy *HostCopy `json:"copy,omitempty" yaml:"copy,omitempty"` - HostKernelConfigs *HostKernelConfigs `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"` - HostJournald *HostJournald `json:"journald,omitempty" yaml:"journald,omitempty"` - HostCGroups *HostCGroups `json:"cgroups,omitempty" yaml:"cgroups,omitempty"` - HostDNS *HostDNS `json:"dns,omitempty" yaml:"dns,omitempty"` + CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"` + Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"` + TCPLoadBalancer *TCPLoadBalancer `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"` + HTTPLoadBalancer *HTTPLoadBalancer `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"` + TCPPortStatus *TCPPortStatus `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"` + UDPPortStatus *UDPPortStatus `json:"udpPortStatus,omitempty" yaml:"udpPortStatus,omitempty"` + Kubernetes *Kubernetes `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"` + IPV4Interfaces *IPV4Interfaces `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"` + SubnetAvailable *SubnetAvailable `json:"subnetAvailable,omitempty" yaml:"subnetAvailable,omitempty"` + DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"` + HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"` + Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"` + BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"` + SystemPackages *HostSystemPackages `json:"systemPackages,omitempty" yaml:"systemPackages,omitempty"` + KernelModules *HostKernelModules `json:"kernelModules,omitempty" yaml:"kernelModules,omitempty"` + TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"` + FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"` + Certificate *Certificate `json:"certificate,omitempty" yaml:"certificate,omitempty"` + CertificatesCollection *HostCertificatesCollection `json:"certificatesCollection,omitempty" yaml:"certificatesCollection,omitempty"` + HostServices *HostServices `json:"hostServices,omitempty" yaml:"hostServices,omitempty"` + HostOS *HostOS `json:"hostOS,omitempty" yaml:"hostOS,omitempty"` + HostRun *HostRun `json:"run,omitempty" yaml:"run,omitempty"` + HostCopy *HostCopy `json:"copy,omitempty" yaml:"copy,omitempty"` + HostKernelConfigs *HostKernelConfigs `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"` + HostJournald *HostJournald `json:"journald,omitempty" yaml:"journald,omitempty"` + HostCGroups *HostCGroups `json:"cgroups,omitempty" yaml:"cgroups,omitempty"` + HostDNS *HostDNS `json:"dns,omitempty" yaml:"dns,omitempty"` + NetworkNamespaceConnectivity *HostNetworkNamespaceConnectivity `json:"networkNamespaceConnectivity,omitempty" yaml:"networkNamespaceConnectivity,omitempty"` } // GetName gets the name of the collector diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 8fb1ac586..a7e810864 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -1920,6 +1920,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) { *out = new(JsonCompare) (*in).DeepCopyInto(*out) } + if in.NetworkNamespaceConnectivity != nil { + in, out := &in.NetworkNamespaceConnectivity, &out.NetworkNamespaceConnectivity + *out = new(NetworkNamespaceConnectivityAnalyze) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze. @@ -2150,6 +2155,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) { *out = new(HostDNS) (*in).DeepCopyInto(*out) } + if in.NetworkNamespaceConnectivity != nil { + in, out := &in.NetworkNamespaceConnectivity, &out.NetworkNamespaceConnectivity + *out = new(HostNetworkNamespaceConnectivity) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect. @@ -2414,6 +2424,22 @@ func (in *HostKernelModules) DeepCopy() *HostKernelModules { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostNetworkNamespaceConnectivity) DeepCopyInto(out *HostNetworkNamespaceConnectivity) { + *out = *in + in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostNetworkNamespaceConnectivity. +func (in *HostNetworkNamespaceConnectivity) DeepCopy() *HostNetworkNamespaceConnectivity { + if in == nil { + return nil + } + out := new(HostNetworkNamespaceConnectivity) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HostOS) DeepCopyInto(out *HostOS) { *out = *in @@ -3198,6 +3224,33 @@ func (in *MetricRequest) DeepCopy() *MetricRequest { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkNamespaceConnectivityAnalyze) DeepCopyInto(out *NetworkNamespaceConnectivityAnalyze) { + *out = *in + in.AnalyzeMeta.DeepCopyInto(&out.AnalyzeMeta) + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkNamespaceConnectivityAnalyze. +func (in *NetworkNamespaceConnectivityAnalyze) DeepCopy() *NetworkNamespaceConnectivityAnalyze { + if in == nil { + return nil + } + out := new(NetworkNamespaceConnectivityAnalyze) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodeMetrics) DeepCopyInto(out *NodeMetrics) { *out = *in diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go index 767313a5d..ff93e6655 100644 --- a/pkg/collect/host_collector.go +++ b/pkg/collect/host_collector.go @@ -99,6 +99,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str return &CollectHostCGroups{collector.HostCGroups, bundlePath}, true case collector.HostDNS != nil: return &CollectHostDNS{collector.HostDNS, bundlePath}, true + case collector.NetworkNamespaceConnectivity != nil: + return &CollectHostNetworkNamespaceConnectivity{collector.NetworkNamespaceConnectivity, bundlePath}, true default: return nil, false } diff --git a/pkg/collect/host_network_namespace_connectivity.go b/pkg/collect/host_network_namespace_connectivity.go new file mode 100644 index 000000000..354267c18 --- /dev/null +++ b/pkg/collect/host_network_namespace_connectivity.go @@ -0,0 +1,231 @@ +package collect + +import ( + "bytes" + "encoding/json" + "fmt" + "net" + "path/filepath" + "strings" + "sync" + "time" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/namespaces" +) + +// NetworkNamespaceConnectivityInfo is the output of this collector, here we +// have the logs, the information from the source and destination namespaces, +// errors and a success flag. +type NetworkNamespaceConnectivityInfo struct { + FromNamespace string `json:"from_namespace"` + ToNamespace string `json:"to_namespace"` + Errors NetworkNamespaceConnectivityErrors `json:"errors"` + Output NetworkNamespaceConnectivityOutput `json:"output"` + Success bool `json:"success"` +} + +// ErrorMessage returns the error message from the errors field. +func (n *NetworkNamespaceConnectivityInfo) ErrorMessage() string { + return n.Errors.Errors() +} + +// NetworkNamespaceConnectivityErrors is a struct that contains the errors that +// occurred during the network namespace connectivity test +type NetworkNamespaceConnectivityErrors struct { + FromNamespaceCreation string `json:"from_namespace_creation"` + ToNamespaceCreation string `json:"to_namespace_creation"` + UDPClient string `json:"udp_client"` + UDPServer string `json:"udp_server"` + TCPClient string `json:"tcp_client"` + TCPServer string `json:"tcp_server"` +} + +// Errors returns a string representation of the errors found during the +// network namespace connectivity test. +func (e NetworkNamespaceConnectivityErrors) Errors() string { + var sb strings.Builder + if e.FromNamespaceCreation != "" { + sb.WriteString("Failed to create 'from' namespace: ") + sb.WriteString(e.FromNamespaceCreation + "\n") + } + + if e.ToNamespaceCreation != "" { + sb.WriteString("Failed to create 'to' namespace: ") + sb.WriteString(e.ToNamespaceCreation + "\n") + } + + if e.UDPClient != "" { + sb.WriteString("UDP connection failed with: ") + sb.WriteString(e.UDPClient + "\n") + } + + if e.UDPServer != "" { + sb.WriteString("UDP server failed with: ") + sb.WriteString(e.UDPServer + "\n") + } + + if e.TCPClient != "" { + sb.WriteString("TCP connection failed with: ") + sb.WriteString(e.TCPClient + "\n") + } + + if e.TCPServer != "" { + sb.WriteString("TCP server failed with: ") + sb.WriteString(e.TCPServer + "\n") + } + return sb.String() +} + +// NetworkNamespaceConnectivityOutput is a struct that contains the logs from +// the network namespace connectivity collector. +type NetworkNamespaceConnectivityOutput struct { + mtx sync.Mutex + Logs []string `json:"logs"` +} + +// Printf is a method that allows us to print the logs directly into a slice. +func (l *NetworkNamespaceConnectivityOutput) Printf(format string, v ...interface{}) { + l.mtx.Lock() + defer l.mtx.Unlock() + + format = fmt.Sprintf("[%s] %s", time.Now().Format(time.RFC3339), format) + l.Logs = append(l.Logs, fmt.Sprintf(format, v...)) +} + +// CollectHostNetworkNamespaceConnectivity collects information about the +// capability of the host to route traffic between two different network +// namespaces. This collector will create two network namespaces and attempt to +// issue TCP and UDP requests between them. +type CollectHostNetworkNamespaceConnectivity struct { + hostCollector *troubleshootv1beta2.HostNetworkNamespaceConnectivity + BundlePath string +} + +// Title returns the title of the collector. +func (c *CollectHostNetworkNamespaceConnectivity) Title() string { + return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Host Network Namespace Connectivity") +} + +// IsExcluded returns true if the collector should be excluded. +func (c *CollectHostNetworkNamespaceConnectivity) IsExcluded() (bool, error) { + return isExcluded(c.hostCollector.Exclude) +} + +// marshal marshals the network namespace connectivity info into a JSON file, +// writes it to the bundle path and returns the file name and the data. +func (c *CollectHostNetworkNamespaceConnectivity) marshal(info *NetworkNamespaceConnectivityInfo) (map[string][]byte, error) { + collectorName := c.hostCollector.CollectorName + if collectorName == "" { + collectorName = "networkNamespaceConnectivity" + } + name := filepath.Join("host-collectors/system", collectorName+".json") + + data, err := json.MarshalIndent(info, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal network namespace connectivity info: %w", err) + } + + output := NewResult() + output.SaveResult(c.BundlePath, name, bytes.NewBuffer(data)) + + return map[string][]byte{ + name: data, + }, nil +} + +// validateCIDRs validates both the from and to CIDRs. They must be provided, +// be different and be valid CIDRs. +func (c *CollectHostNetworkNamespaceConnectivity) validateCIDRs() error { + if c.hostCollector.FromCIDR == "" || c.hostCollector.ToCIDR == "" { + return fmt.Errorf("fromCIDR and toCIDR must be provided") + } + + if c.hostCollector.FromCIDR == c.hostCollector.ToCIDR { + return fmt.Errorf("fromCIDR and toCIDR must be different") + } + + if _, _, err := net.ParseCIDR(c.hostCollector.FromCIDR); err != nil { + return fmt.Errorf("%s is not a valid cidr: %w", c.hostCollector.FromCIDR, err) + } + + if _, _, err := net.ParseCIDR(c.hostCollector.ToCIDR); err != nil { + return fmt.Errorf("%s is not a valid cidr: %w", c.hostCollector.ToCIDR, err) + } + + return nil +} + +// Collect collects the network namespace connectivity information. This +// function expects both the from and to CIDRs to be provided and different +// from each other. +func (c *CollectHostNetworkNamespaceConnectivity) Collect(progressChan chan<- interface{}) (map[string][]byte, error) { + if err := c.validateCIDRs(); err != nil { + return nil, err + } + + result := &NetworkNamespaceConnectivityInfo{ + FromNamespace: c.hostCollector.FromCIDR, + ToNamespace: c.hostCollector.ToCIDR, + } + + opts := []namespaces.Option{namespaces.WithLogf(result.Output.Printf)} + + // if user has chosen to use a specific port, use it. + if c.hostCollector.Port != 0 { + opts = append(opts, namespaces.WithPort(c.hostCollector.Port)) + } + + // if user has chosen to use a specific timeout and it is valid, use it. + if c.hostCollector.Timeout != "" { + timeout, err := time.ParseDuration(c.hostCollector.Timeout) + if err != nil { + return nil, fmt.Errorf("invalid timeout %s", c.hostCollector.Timeout) + } + result.Output.Printf("using user provided timeout of %q", c.hostCollector.Timeout) + opts = append(opts, namespaces.WithTimeout(timeout)) + } + + fromNS, err := namespaces.NewNamespacePinger("from", c.hostCollector.FromCIDR, opts...) + if err != nil { + result.Errors.ToNamespaceCreation = err.Error() + return c.marshal(result) + } + defer fromNS.Close() + + toNS, err := namespaces.NewNamespacePinger("to", c.hostCollector.ToCIDR, opts...) + if err != nil { + result.Errors.FromNamespaceCreation = err.Error() + return c.marshal(result) + } + defer toNS.Close() + + udpErrors, tcpErrors := make(chan error), make(chan error) + toNS.StartUDPEchoServer(udpErrors) + toNS.StartTCPEchoServer(tcpErrors) + + success := true + if err := fromNS.PingUDP(toNS.InternalIP); err != nil { + result.Errors.UDPClient = err.Error() + success = false + } + + if err := <-udpErrors; err != nil { + result.Errors.UDPServer = err.Error() + success = false + } + + if err := fromNS.PingTCP(toNS.InternalIP); err != nil { + result.Errors.TCPClient = err.Error() + success = false + } + + if err := <-tcpErrors; err != nil { + result.Errors.TCPServer = err.Error() + success = false + } + + result.Success = success + result.Output.Printf("network namespace connectivity test finished") + return c.marshal(result) +} diff --git a/pkg/namespaces/errors.go b/pkg/namespaces/errors.go new file mode 100644 index 000000000..a18f2b431 --- /dev/null +++ b/pkg/namespaces/errors.go @@ -0,0 +1,18 @@ +package namespaces + +import "fmt" + +// WrapIfFail executes the provided function. If the function succeeds it +// simply returns the original error (that can be nil). If the function fails +// then it assesses if an original error was provided and wraps it if true. +// This function is a sugar to be used at when deferring function that can also +// return errors, we don't want to loose any context. +func WrapIfFail(msg string, originalerr error, fn func() error) error { + if fnerr := fn(); fnerr != nil { + if originalerr == nil { + return fmt.Errorf("%s: %w", msg, fnerr) + } + return fmt.Errorf("%s: %w: %w", msg, fnerr, originalerr) + } + return originalerr +} diff --git a/pkg/namespaces/errors_test.go b/pkg/namespaces/errors_test.go new file mode 100644 index 000000000..e46650427 --- /dev/null +++ b/pkg/namespaces/errors_test.go @@ -0,0 +1,57 @@ +package namespaces + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWrapIfFail(t *testing.T) { + for _, tt := range []struct { + name string + msg string + originalerr error + fn func() error + expectederr string + }{ + { + name: "function succeeds, no original error", + msg: "test message", + originalerr: nil, + fn: func() error { return nil }, + expectederr: "", + }, + { + name: "no original error, function fails", + msg: "test message", + originalerr: nil, + fn: func() error { return fmt.Errorf("test error") }, + expectederr: "test error", + }, + { + name: "original error, function succeeds", + msg: "test message", + originalerr: fmt.Errorf("original error"), + fn: func() error { return nil }, + expectederr: "original error", + }, + { + name: "original error, and function fails", + msg: "test message", + originalerr: fmt.Errorf("original error"), + fn: func() error { return fmt.Errorf("func error") }, + expectederr: "test message: func error: original error", + }, + } { + t.Run(tt.name, func(t *testing.T) { + err := WrapIfFail(tt.msg, tt.originalerr, tt.fn) + if tt.expectederr == "" { + assert.NoError(t, err) + return + } + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectederr) + }) + } +} diff --git a/pkg/namespaces/interface-pair.go b/pkg/namespaces/interface-pair.go new file mode 100644 index 000000000..8fd5e37aa --- /dev/null +++ b/pkg/namespaces/interface-pair.go @@ -0,0 +1,91 @@ +//go:build linux + +package namespaces + +import ( + "errors" + "fmt" + "syscall" + + "github.com/vishvananda/netlink" +) + +// InterfacePair represents a pair of virtual ethernets that are connected to +// each other. these are used to connect a network namespace to the outside +// world. +type InterfacePair struct { + nethandler NetlinkHandler + prefix string + in netlink.Link + out netlink.Link + cfg Configuration +} + +// SetExternalIP assigns an ip address to the interface living in the default +// namespace (outside interface). +func (p *InterfacePair) SetExternalIP(outaddr string) error { + addr, err := p.nethandler.ParseAddr(outaddr) + if err != nil { + return fmt.Errorf("error parsing ip: %w", err) + } + + if err := p.nethandler.AddrAdd(p.out, addr); err != nil { + return fmt.Errorf("error assigning ip: %w", err) + } + + if err := p.nethandler.LinkSetUp(p.out); err != nil { + return fmt.Errorf("error bringing up: %w", err) + } + return nil +} + +// Close deletes the interface pair. by deleting one of the interfaces, the +// other is deleted as well. +func (p *InterfacePair) Close() error { + for _, ifc := range []netlink.Link{p.in, p.out} { + if err := p.nethandler.LinkDel(ifc); err != nil { + var scerr syscall.Errno + if errors.As(err, &scerr) && scerr == syscall.ENODEV { + continue + } + return fmt.Errorf("error deleting %s: %w", ifc.Attrs().Name, scerr) + } + } + return nil +} + +// Setup sets up the interface pair. this function will create the veth pair +// and bring the interfaces up. +func (p *InterfacePair) Setup() (err error) { + in := fmt.Sprintf("%s-in", p.prefix) + out := fmt.Sprintf("%s-out", p.prefix) + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{Name: in}, + PeerName: out, + } + + p.cfg.Logf("creating interface pair %q and %q", in, out) + if err := p.nethandler.LinkAdd(veth); err != nil { + return fmt.Errorf("error creating veth pair: %w", err) + } + + if p.in, err = p.nethandler.LinkByName(in); err != nil { + return fmt.Errorf("error finding %s: %w", in, err) + } + + if p.out, err = p.nethandler.LinkByName(out); err != nil { + return fmt.Errorf("error finding %s: %w", out, err) + } + return nil +} + +// NewInterfacePair creates a pair of connected virtual ethernets. interfaces +// are named `prefix-in` and `prefix-out`. +func NewInterfacePair(prefix string, options ...Option) *InterfacePair { + config := NewConfiguration(options...) + return &InterfacePair{ + nethandler: NetlinkHandle{}, + prefix: prefix, + cfg: config, + } +} diff --git a/pkg/namespaces/interface-pair_test.go b/pkg/namespaces/interface-pair_test.go new file mode 100644 index 000000000..a86658f51 --- /dev/null +++ b/pkg/namespaces/interface-pair_test.go @@ -0,0 +1,119 @@ +//go:build linux + +package namespaces + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/vishvananda/netlink" +) + +// MockNetlink is a mock for netlink handler used in InterfacePair. +type MockNetlink struct { + mock.Mock +} + +// Mock methods +func (m *MockNetlink) ParseAddr(addr string) (*netlink.Addr, error) { + args := m.Called(addr) + return args.Get(0).(*netlink.Addr), args.Error(1) +} + +func (m *MockNetlink) AddrAdd(link netlink.Link, addr *netlink.Addr) error { + args := m.Called(link, addr) + return args.Error(0) +} + +func (m *MockNetlink) LinkSetUp(link netlink.Link) error { + args := m.Called(link) + return args.Error(0) +} + +func (m *MockNetlink) LinkDel(link netlink.Link) error { + args := m.Called(link) + return args.Error(0) +} + +func (m *MockNetlink) LinkAdd(link netlink.Link) error { + args := m.Called(link) + return args.Error(0) +} + +func (m *MockNetlink) LinkByName(name string) (netlink.Link, error) { + args := m.Called(name) + return args.Get(0).(netlink.Link), args.Error(1) +} + +func (m *MockNetlink) LinkSetNsFd(link netlink.Link, fd int) error { + args := m.Called(link, fd) + return args.Error(0) +} + +func (m *MockNetlink) RouteAdd(route *netlink.Route) error { + args := m.Called(route) + return args.Error(0) +} + +func TestInterfacePairSetExternalIP(t *testing.T) { + mockNetlink := &MockNetlink{} + pair := InterfacePair{ + prefix: "test", + nethandler: mockNetlink, + } + + result := &netlink.Addr{ + IPNet: &net.IPNet{ + IP: net.ParseIP("10.0.0.1"), + }, + } + + mockNetlink.On("ParseAddr", "10.0.0.1").Return(result, nil) + mockNetlink.On("AddrAdd", mock.Anything, mock.Anything).Return(nil) + mockNetlink.On("LinkSetUp", mock.Anything).Return(nil) + + err := pair.SetExternalIP("10.0.0.1") + assert.NoError(t, err) + mockNetlink.AssertCalled(t, "ParseAddr", "10.0.0.1") + mockNetlink.AssertCalled(t, "AddrAdd", mock.Anything, mock.Anything) + mockNetlink.AssertCalled(t, "LinkSetUp", mock.Anything, mock.Anything) + +} + +func TestInterfacePairClose(t *testing.T) { + mockNetlink := &MockNetlink{} + pair := InterfacePair{nethandler: mockNetlink} + + mockNetlink.On("LinkDel", mock.Anything).Return(nil) + + err := pair.Close() + assert.NoError(t, err) + mockNetlink.AssertCalled(t, "LinkDel", mock.Anything) +} + +func TestInterfacePairSetup(t *testing.T) { + mockNetlink := &MockNetlink{} + pair := InterfacePair{ + prefix: "test", + nethandler: mockNetlink, + cfg: NewConfiguration(), + } + + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{Name: "test-in"}, + PeerName: "test-out", + } + + mockNetlink.On("LinkAdd", veth).Return(nil) + mockNetlink.On("LinkByName", "test-in").Return(&netlink.Device{}, nil) + mockNetlink.On("LinkByName", "test-out").Return(&netlink.Device{}, nil) + + err := pair.Setup() + assert.NoError(t, err) + + mockNetlink.AssertCalled(t, "LinkAdd", veth) + mockNetlink.AssertCalled(t, "LinkByName", "test-in") + mockNetlink.AssertCalled(t, "LinkByName", "test-out") +} diff --git a/pkg/namespaces/managed-namespace.go b/pkg/namespaces/managed-namespace.go new file mode 100644 index 000000000..da109abc0 --- /dev/null +++ b/pkg/namespaces/managed-namespace.go @@ -0,0 +1,110 @@ +//go:build linux + +package namespaces + +import ( + "fmt" + "net" + + "github.com/apparentlymart/go-cidr/cidr" +) + +// ManagedNetworkNamespace is a struct that helps up setting up a namespace +// with a pre-defined configuration. See NewManagedNetworkNamespace for more +// information on how the namespace is configured. +type ManagedNetworkNamespace struct { + *NetworkNamespace + *InterfacePair + + InternalIP net.IP + ExternalIP net.IP + cfg Configuration +} + +// NewManagedNetworkNamespace creates a new configured network namespace. This +// network namespace will have an interface configurwed with the first ip +// address of the provided cidr. The external interface (living in the default +// namespace) will be configured with the last ip address of the provided cidr +// and will be set as the default gateway for the namespace. +func NewManagedNetworkNamespace(name, cidraddr string, options ...Option) (*ManagedNetworkNamespace, error) { + config := NewConfiguration(options...) + config.Logf("creating network namespace %q with cidr %q", name, cidraddr) + + _, netaddr, err := net.ParseCIDR(cidraddr) + if err != nil { + return nil, fmt.Errorf("failed to parse cidr: %w", err) + } + + // AddressRange() returns the first and the last addresses of the cidrs. + // Those aren't useful as the first is the network address and the last + // is the broadcast address. We need to adjust them here. + netsize, _ := netaddr.Mask.Size() + first, last := cidr.AddressRange(netaddr) + first, last = cidr.Inc(first), cidr.Dec(last) + config.Logf("network namespace %q address range: %q - %q", name, first, last) + + pair := NewInterfacePair(name, options...) + if err := pair.Setup(); err != nil { + return nil, fmt.Errorf("error creating interface pair: %w", err) + } + + fulladdr := fmt.Sprintf("%s/%d", last, netsize) + if err := pair.SetExternalIP(fulladdr); err != nil { + pair.Close() + return nil, fmt.Errorf("error setting external interface: %w", err) + } + + namespace := NewNetworkNamespace(name, options...) + if err := namespace.Setup(); err != nil { + pair.Close() + return nil, fmt.Errorf("error creating namespace: %w", err) + } + + if err := namespace.AttachInterface(pair.in.Attrs().Name); err != nil { + pair.Close() + namespace.Close() + return nil, fmt.Errorf("error attaching interface pair: %w", err) + } + + fulladdr = fmt.Sprintf("%s/%d", first, netsize) + if err := namespace.SetInterfaceIP(pair.in.Attrs().Name, fulladdr); err != nil { + pair.Close() + namespace.Close() + return nil, fmt.Errorf("error setting interface ip: %w", err) + } + + for _, ifname := range []string{"lo", pair.in.Attrs().Name} { + if err := namespace.BringInterfaceUp(ifname); err != nil { + pair.Close() + namespace.Close() + return nil, fmt.Errorf("error bringing %s interface up: %w", ifname, err) + } + } + + if err := namespace.SetDefaultGateway(last.String()); err != nil { + pair.Close() + namespace.Close() + return nil, fmt.Errorf("error setting default gateway: %w", err) + } + + return &ManagedNetworkNamespace{ + NetworkNamespace: namespace, + InterfacePair: pair, + InternalIP: first, + ExternalIP: last, + cfg: config, + }, nil +} + +// Close destroys both the interface pair and the namespace. Here we only need +// to worry about deleting the namespace as the veth pair will be deleted +// automatically. +func (n *ManagedNetworkNamespace) Close() error { + if err := n.InterfacePair.Close(); err != nil { + return fmt.Errorf("error closing interface pair: %w", err) + } + if err := n.NetworkNamespace.Close(); err != nil { + return fmt.Errorf("error closing namespace: %w", err) + } + return nil +} diff --git a/pkg/namespaces/managed-namespace_unsupported.go b/pkg/namespaces/managed-namespace_unsupported.go new file mode 100644 index 000000000..6f2c91b97 --- /dev/null +++ b/pkg/namespaces/managed-namespace_unsupported.go @@ -0,0 +1,42 @@ +//go:build !linux + +package namespaces + +import ( + "fmt" + "net" + "runtime" +) + +type NamespacePinger struct { + InternalIP net.IP + ExternalIP net.IP +} + +func (n *NamespacePinger) Close() error { + return fmt.Errorf("namespaces not supported on %s platform", runtime.GOOS) +} + +func (n *NamespacePinger) PingUDP(_ net.IP) error { + return fmt.Errorf("namespaces not supported on %s platform", runtime.GOOS) +} + +func (n *NamespacePinger) PingTCP(_ net.IP) error { + return fmt.Errorf("namespaces not supported on %s platform", runtime.GOOS) +} + +func (n *NamespacePinger) StartTCPEchoServer(errors chan error) { + go func() { + errors <- fmt.Errorf("namespaces not supported on %s platform", runtime.GOOS) + }() +} + +func (n *NamespacePinger) StartUDPEchoServer(errors chan error) { + go func() { + errors <- fmt.Errorf("namespaces not supported on %s platform", runtime.GOOS) + }() +} + +func NewNamespacePinger(_, _ string, _ ...Option) (*NamespacePinger, error) { + return nil, fmt.Errorf("namespaces not supported on %s platform", runtime.GOOS) +} diff --git a/pkg/namespaces/namespace-handler.go b/pkg/namespaces/namespace-handler.go new file mode 100644 index 000000000..5822cb3ec --- /dev/null +++ b/pkg/namespaces/namespace-handler.go @@ -0,0 +1,38 @@ +//go:build linux + +package namespaces + +import "github.com/vishvananda/netns" + +// NamespaceHandler is an interface that represents the netns functions that +// we need to mock. This only exists for test purposes. +type NamespaceHandler interface { + DeleteNamed(string) error + Set(netns.NsHandle) error + Get() (netns.NsHandle, error) + NewNamed(string) (netns.NsHandle, error) +} + +// NamespaceHandle is a struct that exists solely for the purpose of mocking +// netns functions on tests. It just wraps calls to the netns package. +type NamespaceHandle struct{} + +// DeleteNamed calls netns.DeleteNamed. +func (n NamespaceHandle) DeleteNamed(name string) error { + return netns.DeleteNamed(name) +} + +// Set calls netns.Set. +func (n NamespaceHandle) Set(ns netns.NsHandle) error { + return netns.Set(ns) +} + +// Get calls netns.Get. +func (n NamespaceHandle) Get() (netns.NsHandle, error) { + return netns.Get() +} + +// NewNamed calls netns.NewNamed. +func (n NamespaceHandle) NewNamed(name string) (netns.NsHandle, error) { + return netns.NewNamed(name) +} diff --git a/pkg/namespaces/namespace-pinger.go b/pkg/namespaces/namespace-pinger.go new file mode 100644 index 000000000..b6f23d720 --- /dev/null +++ b/pkg/namespaces/namespace-pinger.go @@ -0,0 +1,217 @@ +//go:build linux + +package namespaces + +import ( + "fmt" + "net" + "time" +) + +type NamespacePinger struct { + *ManagedNetworkNamespace + cfg Configuration +} + +// PingUDP communicates with the provided IP address from within the namespace. +// This functions sends an UDP packet and expects to receive an echo back. +func (n *NamespacePinger) PingUDP(dst net.IP) error { + n.cfg.Logf("reaching to %q from %q with udp", dst, n.InternalIP) + pinger := func() error { + addr := &net.UDPAddr{IP: dst, Port: n.cfg.Port} + conn, err := net.DialUDP("udp", nil, addr) + if err != nil { + return fmt.Errorf("error dialing udp: %w", err) + } + defer conn.Close() + + if _, err = conn.Write([]byte("echo")); err != nil { + return fmt.Errorf("error writing to udp socket: %w", err) + } + + deadline := time.Now().Add(n.cfg.Timeout) + if err := conn.SetReadDeadline(deadline); err != nil { + return fmt.Errorf("error setting udp read deadline: %w", err) + } + + // XXX: review this buffer size and validation. + buffer := make([]byte, 6) + if _, _, err = conn.ReadFromUDP(buffer); err != nil { + return fmt.Errorf("error reading from udp socket: %w", err) + } + + return nil + } + + return n.Run(pinger) +} + +// PingTCP communicates with the provided IP address from within the namespace. +// This functions sends an TCP packet and expects to receive an echo back. +func (n *NamespacePinger) PingTCP(dst net.IP) error { + n.cfg.Logf("reaching to %q from %q with tcp", dst, n.InternalIP) + pinger := func() error { + addr := fmt.Sprintf("%s:%d", dst, n.cfg.Port) + conn, err := net.DialTimeout("tcp", addr, n.cfg.Timeout) + if err != nil { + return fmt.Errorf("error dialing tcp: %w", err) + } + defer conn.Close() + + if _, err = conn.Write([]byte("echo")); err != nil { + return fmt.Errorf("error writing to tcp socket: %w", err) + } + + // XXX: review this buffer size and validation. + buffer := make([]byte, 6) + if _, err = conn.Read(buffer); err != nil { + return fmt.Errorf("error reading from tcp socket: %w", err) + } + + return nil + } + return n.Run(pinger) +} + +// StartTCPEchoServer is a helper to run startTCPEchoServer inside a goroutine. +// This function blocks until the server is ready to receive packets or failed +// to start. Errors are sent to the provided channel. +func (n *NamespacePinger) StartTCPEchoServer(errors chan error) { + ready := make(chan struct{}) + go func() { + errors <- n.startTCPEchoServer(ready) + }() + <-ready +} + +// startTCPEchoServer starts a tcp server inside the namespace. The thread +// running the goroutine that process this call will be moved to the namespace. +// This echo servers just returns "echo" as a response. Once one packet is +// received, the server ends. Callers must wait until the ready channel is +// closed before they can start sending packets. +func (n *NamespacePinger) startTCPEchoServer(ready chan struct{}) (err error) { + addr := fmt.Sprintf("%s:%d", n.InternalIP, n.cfg.Port) + n.cfg.Logf("starting tcp echo server on namespace %q(%q)", n.name, addr) + + if err = n.Join(); err != nil { + close(ready) + return fmt.Errorf("error joining namespace: %w", err) + } + + defer func() { + err = WrapIfFail("error leaving namespace", err, n.Leave) + }() + + listener, err := net.Listen("tcp", addr) + if err != nil { + close(ready) + return fmt.Errorf("error starting tcp server: %w", err) + } + defer listener.Close() + + deadline := time.Now().Add(n.cfg.Timeout) + tcplistener := listener.(*net.TCPListener) + if err = tcplistener.SetDeadline(deadline); err != nil { + close(ready) + return fmt.Errorf("error setting tcp listener deadline: %w", err) + } + + go func() { + // XXX: here be dragons. we can't signalize we are ready until + // the call to read is done so we artificially sleep for a bit + // here. + time.Sleep(100 * time.Millisecond) + close(ready) + }() + + var conn net.Conn + if conn, err = listener.Accept(); err != nil { + return fmt.Errorf("error accepting connection: %w", err) + } + + n.cfg.Logf("received tcp packet on %q from %q", n.InternalIP, conn.RemoteAddr()) + + if _, err = conn.Write([]byte("echo\n")); err != nil { + return fmt.Errorf("error writing to tcp socket: %w", err) + } + + return nil +} + +// StartUDPEchoServer is a helper to run startUDPTCPEchoServer inside a goroutine. +// This function blocks until the server is ready to receive packets or failed +// to start. Errors are sent to the provided channel. +func (n *NamespacePinger) StartUDPEchoServer(errors chan error) { + ready := make(chan struct{}) + go func() { + errors <- n.startUDPEchoServer(ready) + }() + <-ready +} + +// startUDPEchoServers starts an udp server inside the namespace. The thread +// running the goroutine that process this call will be moved to the namespace. +// This echo servers just returns "echo" as a response. Once one packet is +// received, the server ends. Callers must wait until the ready channel is +// closed before they can start sending packets. +func (n *NamespacePinger) startUDPEchoServer(ready chan struct{}) (err error) { + addr := net.UDPAddr{Port: n.cfg.Port, IP: n.InternalIP} + n.cfg.Logf("starting udp echo server on namespace %q(%q)", n.name, addr.String()) + + if err = n.Join(); err != nil { + close(ready) + return fmt.Errorf("error joining namespace: %w", err) + } + + defer func() { + err = WrapIfFail("error leaving namespace", err, n.Leave) + }() + + conn, err := net.ListenUDP("udp", &addr) + if err != nil { + close(ready) + return fmt.Errorf("error starting udp server: %w", err) + } + defer conn.Close() + + deadline := time.Now().Add(n.cfg.Timeout) + if err = conn.SetDeadline(deadline); err != nil { + close(ready) + return fmt.Errorf("error setting udp listener deadline: %w", err) + } + + go func() { + // XXX: here be dragons. we can't signalize we are ready until + // the call to read is done so we artificially sleep for a bit + // here. + time.Sleep(100 * time.Millisecond) + close(ready) + }() + + var source *net.UDPAddr + var buffer = make([]byte, 1024) + if _, source, err = conn.ReadFromUDP(buffer); err != nil { + return fmt.Errorf("error reading from udp socket: %w", err) + } + + n.cfg.Logf("received udp packet on %q from %q", n.InternalIP, source.AddrPort()) + + if _, err = conn.WriteToUDP([]byte("echo"), source); err != nil { + return fmt.Errorf("error writing to udp socket: %w", err) + } + + return nil +} + +func NewNamespacePinger(name, cidraddr string, options ...Option) (*NamespacePinger, error) { + config := NewConfiguration(options...) + + namespace, err := NewManagedNetworkNamespace(name, cidraddr, options...) + if err != nil { + return nil, fmt.Errorf("error creating network namespace: %w", err) + } + return &NamespacePinger{ + ManagedNetworkNamespace: namespace, + cfg: config, + }, nil +} diff --git a/pkg/namespaces/netlink-handler.go b/pkg/namespaces/netlink-handler.go new file mode 100644 index 000000000..8dba5af75 --- /dev/null +++ b/pkg/namespaces/netlink-handler.go @@ -0,0 +1,62 @@ +//go:build linux + +package namespaces + +import "github.com/vishvananda/netlink" + +// NetlinkHandler is an interface that represents the netlink functions that +// we need to mock. This only exists for test purposes. +type NetlinkHandler interface { + ParseAddr(string) (*netlink.Addr, error) + AddrAdd(netlink.Link, *netlink.Addr) error + LinkSetUp(netlink.Link) error + LinkDel(netlink.Link) error + LinkAdd(netlink.Link) error + LinkByName(string) (netlink.Link, error) + LinkSetNsFd(netlink.Link, int) error + RouteAdd(*netlink.Route) error +} + +// NetlinkHandle is a struct that exists solely for the purpose of mocking +// netlink functions on tests. +type NetlinkHandle struct{} + +// ParseAddr calls netlink.ParseAddr. +func (n NetlinkHandle) ParseAddr(s string) (*netlink.Addr, error) { + return netlink.ParseAddr(s) +} + +// AddrAdd calls netlink.AddrAdd. +func (n NetlinkHandle) AddrAdd(l netlink.Link, a *netlink.Addr) error { + return netlink.AddrAdd(l, a) +} + +// LinkSetUp calls netlink.LinkSetUp. +func (n NetlinkHandle) LinkSetUp(link netlink.Link) error { + return netlink.LinkSetUp(link) +} + +// LinkDel calls netlink.LinkDel. +func (n NetlinkHandle) LinkDel(link netlink.Link) error { + return netlink.LinkDel(link) +} + +// LinkAdd calls netlink.LinkAdd. +func (n NetlinkHandle) LinkAdd(link netlink.Link) error { + return netlink.LinkAdd(link) +} + +// LinkByName calls netlink.LinkByName. +func (n NetlinkHandle) LinkByName(name string) (netlink.Link, error) { + return netlink.LinkByName(name) +} + +// LinkSetNsFd calls netlink.LinkSetNsFd. +func (n NetlinkHandle) LinkSetNsFd(link netlink.Link, fd int) error { + return netlink.LinkSetNsFd(link, fd) +} + +// RouteAdd calls netlink.RouteAdd. +func (n NetlinkHandle) RouteAdd(route *netlink.Route) error { + return netlink.RouteAdd(route) +} diff --git a/pkg/namespaces/network-namespace.go b/pkg/namespaces/network-namespace.go new file mode 100644 index 000000000..65d36323e --- /dev/null +++ b/pkg/namespaces/network-namespace.go @@ -0,0 +1,242 @@ +//go:build linux + +package namespaces + +import ( + "fmt" + "net" + "runtime" + "sync" + "syscall" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" +) + +// NetworkNamespace represents a network namespace. +type NetworkNamespace struct { + nethandler NetlinkHandler + nshandler NamespaceHandler + handle netns.NsHandle + mutex sync.Mutex + origins map[int]netns.NsHandle + name string + cfg Configuration +} + +// Close closes and deletes the network namespace. +func (n *NetworkNamespace) Close() error { + if err := n.handle.Close(); err != nil { + return fmt.Errorf("error closing namespace: %w", err) + } + if err := n.nshandler.DeleteNamed(n.name); err != nil { + return fmt.Errorf("error deleting namespace: %w", err) + } + return nil +} + +// AttachInterface attaches the the provided interface into the namespace. This +// function does not bring the interface up. +func (n *NetworkNamespace) AttachInterface(ifname string) error { + n.cfg.Logf("attaching interface %q to namespace %q", ifname, n.name) + iface, err := n.nethandler.LinkByName(ifname) + if err != nil { + return fmt.Errorf("error finding interface: %w", err) + } + + // put the `in` interface into the namespace. + if err := n.nethandler.LinkSetNsFd(iface, int(n.handle)); err != nil { + return fmt.Errorf("error moving peer into namespace: %w", err) + } + + return nil +} + +// Leave makes the thread leave the namespace. This function returns the thread +// to the previous namespace. Leaves() can't be called without Joining first. +// This function unlocks the current OS thread so it can be used again by +// multiple goroutines. +func (n *NetworkNamespace) Leave() error { + n.mutex.Lock() + defer n.mutex.Unlock() + + var origin netns.NsHandle + var ok bool + + threadID := syscall.Gettid() + if origin, ok = n.origins[threadID]; !ok { + return fmt.Errorf("error leaving namespace: namespace not joined") + } + + if err := n.nshandler.Set(origin); err != nil { + return fmt.Errorf("error switching to original namespace: %w", err) + } + + if err := origin.Close(); err != nil { + return fmt.Errorf("error closing original namespace: %w", err) + } + + delete(n.origins, threadID) + runtime.UnlockOSThread() + return nil +} + +// Join makes the thread join the namespace. The current thread is saved in the +// origin field. Callers are responsible for calling Leave() once they are +// done. This namespace can only be joined once and this is by design. You need +// to Leave() before Joining again. The current OS thread will be locked to the +// namespace. +func (n *NetworkNamespace) Join() (err error) { + n.mutex.Lock() + defer n.mutex.Unlock() + + runtime.LockOSThread() + defer func() { + if err != nil { + runtime.UnlockOSThread() + } + }() + + threadID := syscall.Gettid() + if _, ok := n.origins[threadID]; ok { + return fmt.Errorf("error joining namespace: namespace already joined") + } + + origin, err := n.nshandler.Get() + if err != nil { + return fmt.Errorf("error getting current namespace: %w", err) + } + + if err := n.nshandler.Set(n.handle); err != nil { + return fmt.Errorf("error switching to the namespace: %w", err) + } + + n.origins[threadID] = origin + return nil +} + +// SetInterfaceIP sets the ip address for the provided interface. +func (n *NetworkNamespace) SetInterfaceIP(ifname, ipaddr string) error { + addr, err := n.nethandler.ParseAddr(ipaddr) + if err != nil { + return fmt.Errorf("error parsing ip: %w", err) + } + + // this function will be executed inside the namespace. + fn := func() error { + iface, err := n.nethandler.LinkByName(ifname) + if err != nil { + return err + } + return n.nethandler.AddrAdd(iface, addr) + } + + if err := n.Run(fn); err != nil { + return fmt.Errorf("error setting interface ip: %w", err) + } + + return nil +} + +// BringInterfaceUp brings the provided interface up inside the namespace. +func (n *NetworkNamespace) BringInterfaceUp(ifname string) error { + fn := func() error { + iface, err := n.nethandler.LinkByName(ifname) + if err != nil { + return err + } + return n.nethandler.LinkSetUp(iface) + } + + if err := n.Run(fn); err != nil { + return fmt.Errorf("error bringing interface up: %w", err) + } + + return nil +} + +// SetDefaultGateway sets the default gateway for the namespace. +func (n *NetworkNamespace) SetDefaultGateway(addr string) error { + n.cfg.Logf("setting default gateway %q for namespace %q", addr, n.name) + gw := net.ParseIP(addr) + if gw == nil { + return fmt.Errorf("error parsing invalid gateway: %s", addr) + } + + if err := n.Run( + func() error { + route := netlink.Route{Gw: gw} + return n.nethandler.RouteAdd(&route) + }, + ); err != nil { + return fmt.Errorf("error setting default gateway: %w", err) + } + + return nil +} + +// Run runs the provided function inside the namespace. Restores the original +// namespace once the function has finished. +func (n *NetworkNamespace) Run(f func() error) (err error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var origin netns.NsHandle + if origin, err = n.nshandler.Get(); err != nil { + return fmt.Errorf("error getting current namespace: %w", err) + } + + defer func() { + err = WrapIfFail("error closing namespace", err, origin.Close) + }() + + if err := n.nshandler.Set(n.handle); err != nil { + return fmt.Errorf("error switching to namespace: %w", err) + } + + defer func() { + setter := func() error { return n.nshandler.Set(origin) } + err = WrapIfFail("error exiting namespace", err, setter) + }() + + return f() +} + +func (n *NetworkNamespace) Setup() (err error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var origin netns.NsHandle + if origin, err = n.nshandler.Get(); err != nil { + return fmt.Errorf("error getting current namespace: %w", err) + } + + defer func() { + err = WrapIfFail("error closing original namespace", err, origin.Close) + }() + + var handle netns.NsHandle + if handle, err = n.nshandler.NewNamed(n.name); err != nil { + return fmt.Errorf("error creating network namespace: %w", err) + } + + defer func() { + setter := func() error { return n.nshandler.Set(origin) } + err = WrapIfFail("error exiting namespace", err, setter) + }() + + n.handle = handle + return nil +} + +// NewNetworkNamespace creates a new network namespace. once the namespace is +// created this function restores the thread to the original namespace. +func NewNetworkNamespace(name string, options ...Option) *NetworkNamespace { + return &NetworkNamespace{ + nethandler: NetlinkHandle{}, + nshandler: NamespaceHandle{}, + name: name, + cfg: NewConfiguration(options...), + origins: map[int]netns.NsHandle{}, + } +} diff --git a/pkg/namespaces/network-namespace_test.go b/pkg/namespaces/network-namespace_test.go new file mode 100644 index 000000000..151684e5b --- /dev/null +++ b/pkg/namespaces/network-namespace_test.go @@ -0,0 +1,248 @@ +//go:build linux + +package namespaces + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" +) + +type MockNamespace struct { + mock.Mock +} + +func (m *MockNamespace) DeleteNamed(name string) error { + args := m.Called(name) + return args.Error(0) +} + +func (m *MockNamespace) Set(ns netns.NsHandle) error { + args := m.Called(ns) + return args.Error(0) +} + +func (m *MockNamespace) Get() (netns.NsHandle, error) { + args := m.Called() + return args.Get(0).(netns.NsHandle), args.Error(1) +} + +func (m *MockNamespace) NewNamed(name string) (netns.NsHandle, error) { + args := m.Called(name) + return args.Get(0).(netns.NsHandle), args.Error(1) +} + +func TestNetworkNamespaceAttachInterface(t *testing.T) { + mockNetlink := &MockNetlink{} + + ns := NewNetworkNamespace("test") + ns.nethandler = mockNetlink + + mockNetlink.On("LinkByName", "test-in").Return(&netlink.Device{}, nil) + mockNetlink.On("LinkSetNsFd", mock.Anything, mock.Anything).Return(nil) + + err := ns.AttachInterface("test-in") + assert.NoError(t, err, "error attaching interface") + + mockNetlink.AssertCalled(t, "LinkByName", "test-in") + mockNetlink.AssertCalled(t, "LinkSetNsFd", mock.Anything, mock.Anything) +} + +func TestNetworkNamespaceJoin(t *testing.T) { + mockNetlink := &MockNetlink{} + mockNamespace := &MockNamespace{} + + ns := NewNetworkNamespace("test") + ns.nethandler = mockNetlink + ns.nshandler = mockNamespace + + mockNamespace.On("Get").Return(netns.NsHandle(0), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err := ns.Join() + assert.NoError(t, err, "error joining namespace") + assert.NotEmpty(t, ns.origins) + + mockNamespace.AssertCalled(t, "Get") + mockNamespace.AssertCalled(t, "Set", mock.Anything) +} + +func TestNetworkNamespaceLeave(t *testing.T) { + mockNetlink := &MockNetlink{} + mockNamespace := &MockNamespace{} + + ns := NewNetworkNamespace("test") + ns.nethandler = mockNetlink + ns.nshandler = mockNamespace + + mockNamespace.On("Get").Return(netns.NsHandle(1), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err := ns.Join() + assert.NoError(t, err, "error joining namespace") + assert.NotEmpty(t, ns.origins) + + err = ns.Leave() + assert.NoError(t, err, "error leaving namespace") + assert.Empty(t, ns.origins) + + mockNamespace.AssertCalled(t, "Get") + mockNamespace.AssertCalled(t, "Set", mock.Anything) +} + +func TestNetworkNamespaceSetInterfaceIP(t *testing.T) { + mockNetlink := &MockNetlink{} + mockNamespace := &MockNamespace{} + + ns := NewNetworkNamespace("test") + ns.nethandler = mockNetlink + ns.nshandler = mockNamespace + + mockNetlink.On("ParseAddr", "10.0.0.1").Return(&netlink.Addr{}, nil) + mockNetlink.On("LinkByName", "test-in").Return(&netlink.Device{}, nil) + mockNetlink.On("AddrAdd", mock.Anything, mock.Anything).Return(nil) + + mockNamespace.On("Get").Return(netns.NsHandle(0), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err := ns.SetInterfaceIP("test-in", "10.0.0.1") + assert.NoError(t, err, "error setting interface ip") + + mockNetlink.AssertCalled(t, "ParseAddr", "10.0.0.1") + mockNetlink.AssertCalled(t, "LinkByName", "test-in") + mockNetlink.AssertCalled(t, "AddrAdd", mock.Anything, mock.Anything) + mockNamespace.AssertCalled(t, "Get") + mockNamespace.AssertCalled(t, "Set", mock.Anything) +} + +func TestNetworkNamespaceBringInterfaceUp(t *testing.T) { + mockNetlink := &MockNetlink{} + mockNamespace := &MockNamespace{} + + ns := NewNetworkNamespace("test") + ns.nethandler = mockNetlink + ns.nshandler = mockNamespace + + mockNetlink.On("LinkByName", "test-in").Return(&netlink.Device{}, nil) + mockNetlink.On("LinkSetUp", mock.Anything).Return(nil) + + fd, err := os.CreateTemp("", "test-in") + assert.NoError(t, err, "error creating temporary file") + defer os.Remove(fd.Name()) + + mockNamespace.On("Get").Return(netns.NsHandle(fd.Fd()), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err = ns.BringInterfaceUp("test-in") + assert.NoError(t, err, "error bringing interface up") + + mockNetlink.AssertCalled(t, "LinkByName", "test-in") + mockNetlink.AssertCalled(t, "LinkSetUp", mock.Anything) + mockNamespace.AssertCalled(t, "Get") + mockNamespace.AssertCalled(t, "Set", mock.Anything) +} + +func TestNetworkManagerSetDefaultGateway(t *testing.T) { + mockNetlink := &MockNetlink{} + mockNamespace := &MockNamespace{} + + ns := NewNetworkNamespace("test") + ns.nethandler = mockNetlink + ns.nshandler = mockNamespace + + fd, err := os.CreateTemp("", "test-in") + assert.NoError(t, err, "error creating temporary file") + defer os.Remove(fd.Name()) + + mockNetlink.On("RouteAdd", mock.Anything).Return(nil) + mockNamespace.On("Get").Return(netns.NsHandle(fd.Fd()), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err = ns.SetDefaultGateway("10.0.0.1") + assert.NoError(t, err, "error setting default gateway") + + mockNetlink.AssertCalled(t, "RouteAdd", mock.Anything) + mockNamespace.AssertCalled(t, "Get") + mockNamespace.AssertCalled(t, "Set", mock.Anything) +} + +func TestNetworkNamespaceRun(t *testing.T) { + mockNamespace := &MockNamespace{} + + ns := NewNetworkNamespace("test") + ns.nshandler = mockNamespace + + fd, err := os.CreateTemp("", "test-in") + assert.NoError(t, err, "error creating temporary file") + defer os.Remove(fd.Name()) + + mockNamespace.On("Get").Return(netns.NsHandle(fd.Fd()), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err = ns.Run(func() error { return nil }) + assert.NoError(t, err, "error running function") + + err = ns.Run(func() error { return fmt.Errorf("test error") }) + assert.Error(t, err, "error running function") + + mockNamespace.AssertCalled(t, "Get") + mockNamespace.AssertCalled(t, "Set", mock.Anything) +} + +func TestNetworkNamespaceSetup(t *testing.T) { + // succeeds to create the namespace. + mockNamespace := &MockNamespace{} + + ns := NewNetworkNamespace("test") + ns.nshandler = mockNamespace + + fd, err := os.CreateTemp("", "test-in") + assert.NoError(t, err, "error creating temporary file") + defer os.Remove(fd.Name()) + + fd2, err := os.CreateTemp("", "test-in") + assert.NoError(t, err, "error creating temporary file") + defer os.Remove(fd2.Name()) + + mockNamespace.On("NewNamed", "test").Return(netns.NsHandle(fd2.Fd()), nil) + mockNamespace.On("Get").Return(netns.NsHandle(fd.Fd()), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err = ns.Setup() + assert.NoError(t, err, "error setting up namespace") + + mockNamespace.AssertCalled(t, "NewNamed", "test") + mockNamespace.AssertCalled(t, "Get") + mockNamespace.AssertCalled(t, "Set", mock.Anything) + + // os fails to create the namespace. + mockNamespace = &MockNamespace{} + ns = NewNetworkNamespace("test") + ns.nshandler = mockNamespace + + mockNamespace.On("NewNamed", "test").Return(netns.NsHandle(0), fmt.Errorf("test error")) + mockNamespace.On("Get").Return(netns.NsHandle(fd.Fd()), nil) + mockNamespace.On("Set", mock.Anything).Return(nil) + + err = ns.Setup() + assert.Error(t, err, "expected error setting up namespace") + + mockNamespace.AssertCalled(t, "NewNamed", "test") + + // fail to open the default namespace. + mockNamespace = &MockNamespace{} + ns = NewNetworkNamespace("test") + ns.nshandler = mockNamespace + + mockNamespace.On("Get").Return(netns.NsHandle(0), fmt.Errorf("test error")) + + err = ns.Setup() + assert.Error(t, err, "expected error setting up namespace") + + mockNamespace.AssertCalled(t, "Get") +} diff --git a/pkg/namespaces/options.go b/pkg/namespaces/options.go new file mode 100644 index 000000000..a1e5730fa --- /dev/null +++ b/pkg/namespaces/options.go @@ -0,0 +1,51 @@ +package namespaces + +import "time" + +// Option is a function that sets an optional configuration. +type Option func(*Configuration) + +// Configuration holds the runtime configuration for this package. +type Configuration struct { + // Logf is a function that will be used to log messages. If not + // provided the default logger will be used. + Logf func(string, ...interface{}) + // Port is the port to use for the UDP and TCP pings. + Port int + // Timeout is the timeout for the UDP and TCP connection to finish. + Timeout time.Duration +} + +// NewConfiguration creates a new configuration with the provided options. +func NewConfiguration(options ...Option) Configuration { + cfg := Configuration{ + Logf: func(string, ...interface{}) {}, + Port: 8080, + Timeout: 5 * time.Second, + } + for _, o := range options { + o(&cfg) + } + return cfg +} + +// WithLogf sets the log function for this package. +func WithLogf(f func(string, ...interface{})) Option { + return func(c *Configuration) { + c.Logf = f + } +} + +// WithPort sets the port to use for the UDP and TCP pings. +func WithPort(port int) Option { + return func(c *Configuration) { + c.Port = port + } +} + +// WithTimeout sets the timeout for the UDP and TCP connections. +func WithTimeout(timeout time.Duration) Option { + return func(c *Configuration) { + c.Timeout = timeout + } +} diff --git a/schemas/analyzer-troubleshoot-v1beta2.json b/schemas/analyzer-troubleshoot-v1beta2.json index 06fc51e1f..f6bd0768e 100644 --- a/schemas/analyzer-troubleshoot-v1beta2.json +++ b/schemas/analyzer-troubleshoot-v1beta2.json @@ -3900,6 +3900,82 @@ } } }, + "networkNamespaceConnectivity": { + "type": "object", + "required": [ + "outcomes" + ], + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "checkName": { + "type": "string" + }, + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "outcomes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fail": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "pass": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "warn": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + } + } + } + }, + "strict": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, "subnetAvailable": { "type": "object", "required": [ diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json index d54ff65a6..200c262b0 100644 --- a/schemas/collector-troubleshoot-v1beta2.json +++ b/schemas/collector-troubleshoot-v1beta2.json @@ -15024,6 +15024,34 @@ } } }, + "networkNamespaceConnectivity": { + "type": "object", + "required": [ + "fromCIDR", + "port", + "toCIDR" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "fromCIDR": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "timeout": { + "type": "string" + }, + "toCIDR": { + "type": "string" + } + } + }, "run": { "type": "object", "required": [ diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index 2c1ac4ff7..9d805017e 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -18355,6 +18355,82 @@ } } }, + "networkNamespaceConnectivity": { + "type": "object", + "required": [ + "outcomes" + ], + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "checkName": { + "type": "string" + }, + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "outcomes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fail": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "pass": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "warn": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + } + } + } + }, + "strict": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, "subnetAvailable": { "type": "object", "required": [ @@ -19536,6 +19612,34 @@ } } }, + "networkNamespaceConnectivity": { + "type": "object", + "required": [ + "fromCIDR", + "port", + "toCIDR" + ], + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "fromCIDR": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "timeout": { + "type": "string" + }, + "toCIDR": { + "type": "string" + } + } + }, "run": { "type": "object", "required": [