feat: implement collector and analyser for network namespace connectivity (#1670)

* feat: implement collector and analyser for network namespace connectivity

checks if two network namespaces can talk to each other on udp and tcp.
its usage is as follows:

```yaml
apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
  name: test
spec:
  hostCollectors:
  - networkNamespaceConnectivity:
      collectorName: check-network-connectivity
      fromCIDR: 10.0.0.0/24
      toCIDR: 10.0.1.0/24
  hostAnalyzers:
  - networkNamespaceConnectivity:
      collectorName: check-network-connectivity
      outcomes:
      - pass:
          message: "Communication between 10.0.0.0/24 and 10.0.1.0/24 is working"
      - fail:
          message: "Communication between 10.0.0.0/24 and 10.0.1.0/24 isn't working"
```

if this fails then you may need to enable `forwarding` with:

```bash
sysctl -w net.ipv4.ip_forward=1
```

if it still fails then you may need to configure firewalld to allow the
traffic or simply disable it for sake of testing.

* chore: rebuild schemas

* chore: remove unused property

* chore: disable namespaces for other platforms

* chore: make sure we timeout temporary servers

* feat: analyzer now supports multi-node collection

* feat: check both udp and tcp even on failure

check both protocols even if one fails. this pr commit also introduces a
timeout that can be set by the user.

* feat: add templating to the failure outcome

allow users to dump the errors found during the analysis.

* chore: addressing pr comments

* feat: delete interface pair before namespace

even though the interface pair is deleted everyttime we delete the
namespace on my tests we better delete it before we delete the
namespace.

this comes out of a review comment where some people seem to still be
able to see the interface pair even after the namespace is deleted.

i.e. better safe than sorry.

* chore: fix typo on comment
This commit is contained in:
Ricardo Maraschini
2024-11-06 11:30:13 +01:00
committed by GitHub
parent ea900a1881
commit a8db999965
29 changed files with 2219 additions and 50 deletions
@@ -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:
@@ -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:
@@ -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:
@@ -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:
@@ -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:
+3
View File
@@ -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
+7
View File
@@ -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=
+2
View File
@@ -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
}
@@ -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
}
@@ -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"`
}
@@ -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
@@ -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
+2
View File
@@ -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
}
@@ -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)
}
+18
View File
@@ -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
}
+57
View File
@@ -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)
})
}
}
+91
View File
@@ -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,
}
}
+119
View File
@@ -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")
}
+110
View File
@@ -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
}
@@ -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)
}
+38
View File
@@ -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)
}
+217
View File
@@ -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
}
+62
View File
@@ -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)
}
+242
View File
@@ -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{},
}
}
+248
View File
@@ -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")
}
+51
View File
@@ -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
}
}
@@ -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": [
@@ -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": [
@@ -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": [